package com.cmc.cloud.cmclink.doc.service.impl.so;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.lang.Validator;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.lock.annotation.Lock4j;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.cmc.cloud.cmclink.bdt.api.base.PortApi;
import com.cmc.cloud.cmclink.bdt.api.base.VesselDataApi;
import com.cmc.cloud.cmclink.bdt.api.base.dto.req.PortListByCodeReqDTO;
import com.cmc.cloud.cmclink.bdt.api.base.dto.req.VesselNameListByCodeReqDTO;
import com.cmc.cloud.cmclink.bdt.api.base.dto.resp.PortListByCodeRespDTO;
import com.cmc.cloud.cmclink.bdt.api.base.dto.resp.VesselNameDataRespDTO;
import com.cmc.cloud.cmclink.doc.api.document.emums.*;
import com.cmc.cloud.cmclink.doc.api.document.rep.manifestrep.manifest.ManifestInfoRespVO;
import com.cmc.cloud.cmclink.doc.api.document.rep.manifestrep.manifest.ManifestRespVO;
import com.cmc.cloud.cmclink.doc.api.document.rep.manifestrep.manifestcargo.ManifestCargoRespVO;
import com.cmc.cloud.cmclink.doc.api.document.rep.manifestrep.manifestcntr.ManifestCntrRespVO;
import com.cmc.cloud.cmclink.doc.api.document.rep.manifestrep.manifestscn.ManifestScnRespVO;
import com.cmc.cloud.cmclink.doc.api.document.rep.manifestrep.manifestvslvoy.ManifestVslvoyRespVO;
import com.cmc.cloud.cmclink.doc.api.document.req.so.BookingConfirmationDataDto;
import com.cmc.cloud.cmclink.doc.api.document.req.so.SoForecastRespVO;
import com.cmc.cloud.cmclink.doc.api.document.req.soamendapplyvo.SoAmendApplyCreateReqVO;
import com.cmc.cloud.cmclink.doc.api.document.req.soamendapplyvo.SoAmendApplyUpdateReqVO;
import com.cmc.cloud.cmclink.doc.configuration.CommonConfiguration;
import com.cmc.cloud.cmclink.doc.constants.errorconstant.BlnoBlPrintLogErrorCodeConstants;
import com.cmc.cloud.cmclink.doc.constants.errorconstant.ManifestErrorCodeConstants;
import com.cmc.cloud.cmclink.doc.constants.errorconstant.SoAmendApplyErrorCodeConstants;
import com.cmc.cloud.cmclink.doc.constants.errorconstant.SoErrorCodeConstants;
import com.cmc.cloud.cmclink.doc.convert.*;
import com.cmc.cloud.cmclink.doc.dto.RestoreSpace;
import com.cmc.cloud.cmclink.doc.dto.SpecialModifyVelVoyDto;
import com.cmc.cloud.cmclink.doc.entity.*;
import com.cmc.cloud.cmclink.doc.enums.filter.FilterConditionFieldEnum;
import com.cmc.cloud.cmclink.doc.mapper.*;
import com.cmc.cloud.cmclink.doc.rpcservice.CustomerRpcService;
import com.cmc.cloud.cmclink.doc.rpcservice.NucTransferService;
import com.cmc.cloud.cmclink.doc.rpcservice.VoyScheduleService;
import com.cmc.cloud.cmclink.doc.service.*;
import com.cmc.cloud.cmclink.doc.service.approvespace.AutoSpaceService;
import com.cmc.cloud.cmclink.doc.service.manifest.ManifestService;
import com.cmc.cloud.cmclink.doc.service.si.BlnoService;
import com.cmc.cloud.cmclink.doc.service.si.BlnoSpiltCombineService;
import com.cmc.cloud.cmclink.doc.service.si.VoyageLockService;
import com.cmc.cloud.cmclink.doc.service.so.*;
import com.cmc.cloud.cmclink.doc.service.special.SpecialModifyVelVoyService;
import com.cmc.cloud.cmclink.doc.util.CheckUtil;
import com.cmc.cloud.cmclink.doc.util.CostChargeUtil;
import com.cmc.cloud.cmclink.doc.util.EmailUtil;
import com.cmc.cloud.cmclink.doc.util.sepcial.PdfUtil;
import com.cmc.cloud.cmclink.doc.util.so.CmclinkExpressionUtil;
import com.cmc.cloud.cmclink.doc.util.so.CompareUtils;
import com.cmc.cloud.cmclink.doc.vo.CodCheckReqVO;
import com.cmc.cloud.cmclink.doc.vo.blnovo.BlnoPageReqVO;
import com.cmc.cloud.cmclink.doc.vo.blnovo.BlnoPageRespVO;
import com.cmc.cloud.cmclink.doc.vo.docbkconfirmprintsetupvo.DocBkConfirmPrintSetupRespVO;
import com.cmc.cloud.cmclink.doc.vo.limitrulevo.LimitRulePageReqVO;
import com.cmc.cloud.cmclink.doc.vo.limitrulevo.LimitRuleRespVO;
import com.cmc.cloud.cmclink.doc.vo.manifestvo.manifest.ManifestSoPageReqVO;
import com.cmc.cloud.cmclink.doc.vo.sovo.attachmentvo.AttachmentCountVO;
import com.cmc.cloud.cmclink.doc.vo.sovo.attachmentvo.AttachmentCreateReqVO;
import com.cmc.cloud.cmclink.doc.vo.sovo.attachmentvo.AttachmentRespVO;
import com.cmc.cloud.cmclink.doc.vo.sovo.attachmentvo.AttachmentUpdateReqVO;
import com.cmc.cloud.cmclink.doc.vo.sovo.operatelog.OperateLogReqVO;
import com.cmc.cloud.cmclink.doc.vo.sovo.requirementsvo.RequirementsCreateReqVO;
import com.cmc.cloud.cmclink.doc.vo.sovo.requirementsvo.RequirementsRespVO;
import com.cmc.cloud.cmclink.doc.vo.sovo.requirementsvo.RequirementsUpdateReqVO;
import com.cmc.cloud.cmclink.doc.vo.sovo.sobcrcheck.SoBcrCheckCreateReqVO;
import com.cmc.cloud.cmclink.doc.vo.sovo.sobcrcheck.SoBcrCheckRespVO;
import com.cmc.cloud.cmclink.doc.vo.sovo.socargovo.*;
import com.cmc.cloud.cmclink.doc.vo.sovo.socontactvo.SoContactCreateReqVO;
import com.cmc.cloud.cmclink.doc.vo.sovo.socontactvo.SoContactUpdateReqVO;
import com.cmc.cloud.cmclink.doc.vo.sovo.soforecastvo.SoForecastCreateReqVO;
import com.cmc.cloud.cmclink.doc.vo.sovo.soforecastvo.SoForecastSumVO;
import com.cmc.cloud.cmclink.doc.vo.sovo.soforecastvo.SoForecastUpdateReqVO;
import com.cmc.cloud.cmclink.doc.vo.sovo.sopaymentvo.SoPaymentRespVO;
import com.cmc.cloud.cmclink.doc.vo.sovo.soscnvo.SoScnCreateReqVO;
import com.cmc.cloud.cmclink.doc.vo.sovo.soscnvo.SoScnRespVO;
import com.cmc.cloud.cmclink.doc.vo.sovo.soscnvo.SoScnUpdateReqVO;
import com.cmc.cloud.cmclink.doc.vo.sovo.sovo.*;
import com.cmc.cloud.cmclink.doc.vo.sovo.sovslvoyvo.*;
import com.cmc.cloud.cmclink.svl.api.base.req.PolPodReqDTO;
import com.cmc.cloud.cmclink.svl.api.base.resp.VesselVoyRespDTO;
import com.cmc.cloud.cmclink.system.api.api.file.FileApi;
import com.cmc.cloud.cmclink.system.api.api.file.dto.FileUploadReqDTO;
import com.cmc.cloud.cmclink.system.api.api.file.dto.FileUploadRespDTO;
import com.cmes.framework.common.exception.ErrorCode;
import com.cmes.framework.common.exception.util.ServiceExceptionUtil;
import com.cmes.framework.common.pojo.CommonResult;
import com.cmes.framework.common.pojo.PageResult;
import com.cmes.framework.ex.Assertion;
import com.cmes.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.cmes.platform.system.nuc.dto.AdminUserDto;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.beanutils.BeanMap;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.redisson.api.RAtomicLong;
import org.redisson.api.RedissonClient;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;
import org.springframework.validation.annotation.Validated;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;

import static com.cmc.cloud.cmclink.doc.service.impl.si.BlnoBlPrintLogServiceImpl.getFileUploadReqDTO;


/**
 * @author 陈正
 * @date 2025/7/22
 */
@Service
@Validated
@Slf4j
public class BizSoServiceImpl implements BizSoService {
    @Resource
    private SoServiceImpl soService;
    @Resource
    private SoVslvoyServiceImpl soVslvoyService;
    @Resource
    private SoCargoServiceImpl soCargoService;
    @Resource
    private SoForecastServiceImpl soForecastService;
    @Resource
    private SoContactServiceImpl soContactService;
    @Resource
    private SoScnServiceImpl soScnService;
    @Resource
    private AttachmentServiceImpl attachmentService;
    @Resource
    private RequirementsServiceImpl requirementsService;
    @Resource
    private BlnoRuleKlcodeService blnoRuleKlcodeService;
    @Resource
    private BlnoRulePbcodeService blnoRulePbcodeService;
    @Resource
    private BlnoRuleYearService blnoRuleYearService;
    @Resource
    RedissonClient redissonClient;
    @Resource
    private BlnoService blnoService;
    @Resource
    VoyageBookingTimeService voyageBookingTimeService;
    @Resource
    LimitRuleService limitRuleService;
    @Resource
    SoBcrCheckService soBcrCheckService;
    @Resource
    OperateLogService operateLogService;
    @Resource
    SoAmendApplyService soAmendApplyService;
    @Resource
    DocBkConfirmPrintSetupService docBkConfirmPrintSetupService;
    @Resource
    PortApi portApi;
    @Resource
    VesselDataApi vesselDataApi;
    @Resource
    private VoyScheduleService voyScheduleService;
    @Resource
    SoCommonService soCommonService;
    @Resource
    private FileApi fileApi;
    @Resource
    private CostChargeUtil costChargeUtil;
    @Resource
    SpecialCargoApplyService specialCargoApplyService;

    @Resource
    VoyageBsaService voyageBsaService;

    @Resource
    VoyageLockService voyageLockService;
    @Resource
    CustomerRpcService nameTransferService;
    @Resource
    ManifestService manifestService;
    @Resource
    AutoSpaceService autoSpaceService;
    @Resource
    BlnoMapper blnoMapper;
    @Resource
    SpecialModifyVelVoyService specialModifyVelVoyService;
    @Resource
    VesselOffHireMaintenanceService vesselOffHireMaintenanceService;
    @Resource
    BlnoFreightMapper blnoFreightMapper;
    @Resource
    BlnoLocalChargeMapper blnoLocalChargeMapper;
    @Resource
    BlnoLocalCostMapper blnoLocalCostMapper;
    @Resource
    private CommonConfiguration configuration;
    @Resource
    private ConfirmNoticeMapper confirmNoticeMapper;
    @Resource
    private BlnoSpiltCombineDetailMapper blnoSpiltCombineDetailMapper;
    @Resource
    private BlnoSpiltCombineMapper blnoSpiltCombineMapper;
    @Resource
    private BlnoSpiltCombineService blnoSpiltCombineService;
    @Resource
    private NucTransferService nucTransferService;
    @Resource
    private EmailUtil emailUtil;
    @Resource
    ThreadPoolTaskExecutor taskExecutor;
    private static final Integer MAX_NUM = 1000000;
    private static final String REDIS_SO_BOOKING_NO = "doc:so:bookingNo:";
    private static final String CARGO_TYPE_DRY = "DRY";
    private static final String ERROR_OPERATE_TYPE = "V";
    private static final String ROUND_TRIP = "round_trip";
    private static final String SI_CUT_OFF = "si_cut_off";
    private static final List<String> NORTHERN_THREE_PORTS = Arrays.asList("CNTAO", "CNTXG", "CNDLC", "CNRZH");
    private static final DateTimeFormatter FORMATTER_AMEND = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
    private static final String CNSHA = "CNSHA";
    private static final String CNNGB = "CNNGB";


    @Override
    @Transactional(rollbackFor = Exception.class)
    @Lock4j(name = "soUnique", keys = {"'createAndSubmit'", "#userId"})
    public Long createAndSubmit(SoCreateAndUpdateReqVo createReqVO, String userId) {
        Long soId = createOrUpdateSo(createReqVO, createReqVO.getSo().getId() != null, userId);
        batchSubmitDraft(Arrays.asList(soId), userId);
        return soId;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @Lock4j(name = "soUnique", keys = {"'createOrUpdateSo'", "#userId"})
    public Long createOrUpdateSo(SoCreateAndUpdateReqVo createReqVO, boolean update, String userId) {
        return createOrUpdateSo(createReqVO, update, userId, true);
    }

    @Transactional(rollbackFor = Exception.class)
    @Lock4j(name = "soUnique", keys = {"'createOrUpdateSo'", "#userId"})
    public Long createOrUpdateSo(SoCreateAndUpdateReqVo createReqVO, boolean update, String userId, boolean insertSpaceApproveStatusNull) {
        SoDetailRespVO oldSo = new SoDetailRespVO();
        checkSoNotNull(createReqVO);
        if (update) {
            //如果状态是【订舱待补充材料】的 SO 只能编辑附件内容。
            //只有状态是【订舱草稿】的 SO 才能编辑 SO。
            List<String> canUpdateSoStatus = Arrays.asList(SoStatusEnum.BookingDraft.getValue(), SoStatusEnum.AwaitingMaterialUploaded.getValue());
            if (createReqVO.getSo() != null && createReqVO.getSo().getId() != null) {
                oldSo = getDetail(createReqVO.getSo().getId(), StringUtils.EMPTY, false);
                checkUserLimit(oldSo.getSo().getCreator(), userId);
            } else {
                throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_NOT_EXISTS);
            }
            if (!canUpdateSoStatus.contains(oldSo.getSo().getSoStatus())) {
                throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_NO_UPDATE);
            }
        } else {
            if (createReqVO.getSo() != null && createReqVO.getSo().getId() != null) {
                throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_HAS_EXISTS);
            }
        }
        if (update && SoStatusEnum.AwaitingMaterialUploaded.getValue().equals(oldSo.getSo().getSoStatus())) {
            attachmentService.createOrUpdateAndDeleteBatch(oldSo.getSo().getId(), SoSiEnum.SO.getValue(), createReqVO.getAttachmentList(), oldSo.getAttachmentList());
            return oldSo.getSo().getId();
        } else {
            checkForbiddenCharacter(createReqVO.getSoScn(), createReqVO.getSoCargoList());
            checkPhoneAndEmail(createReqVO);
            checkSoInfo(createReqVO, insertSpaceApproveStatusNull);
            checkVslvoy(createReqVO.getSoVslvoyList());
            SoDO so = soService.createOrUpdateSo(createReqVO.getSo());
            attachmentService.createOrUpdateAndDeleteBatch(so.getId(), SoSiEnum.SO.getValue(), createReqVO.getAttachmentList(), oldSo.getAttachmentList());
            soVslvoyService.createOrUpdateAndDeleteBatch(so.getId(), createReqVO.getSoVslvoyList(), oldSo.getSoVslvoyList(), insertSpaceApproveStatusNull);
            soCargoService.createOrUpdateAndDeleteBatch(so.getId(), createReqVO.getSoCargoList(), oldSo.getSoCargoList());
            soForecastService.createOrUpdateAndDeleteBatch(so.getId(), createReqVO.getSoForecastList(), oldSo.getSoForecastList());
            soContactService.createOrUpdateAndDeleteBatch(so.getId(), createReqVO.getSoContactList(), oldSo.getSoContactList());
            if (createReqVO.getSoScn() != null) {
                createReqVO.getSoScn().setPartyTypeCode(PartyTypeCodeEnum.SHIPPER.getValue());
                soScnService.createOrUpdateSo(so.getId(), createReqVO.getSoScn(), oldSo.getSoScn());
            }
            requirementsService.createOrUpdateAndDeleteBatch(so.getId(), SoSiEnum.SO.getValue(), createReqVO.getRequirementsList(), oldSo.getRequirementsList());
            return so.getId();
        }
    }

    @Transactional(rollbackFor = Exception.class)
    @Lock4j(name = "soUnique", keys = {"'updateConfirmedSoAmendVessel'", "#userId"})
    public Long updateConfirmedSoAmendVessel(SoCreateAndUpdateReqVo createReqVO) {
        SoDetailRespVO oldSo;
        if (createReqVO.getSo() != null && createReqVO.getOldId() != null) {
            oldSo = getDetail(createReqVO.getOldId(), StringUtils.EMPTY, false);
        } else {
            throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_NOT_EXISTS);
        }
        //变更so对象为新增，id应该为空，由数据库生成
        checkSoInfo(createReqVO, false);
        checkVslvoy(createReqVO.getSoVslvoyList());
        createReqVO.getSo().setId(null);
        SoDO so = soService.createOrUpdateSo(createReqVO.getSo());
        //COD变更申请时只新增一条数据，原来数据不变。oldList传null，不做处理,批舱状态要更新
        soVslvoyService.createOrUpdateAndDeleteBatch(so.getId(), createReqVO.getSoVslvoyList(), null, false);
        //其他子表复制原数据创建新对象，id置空，soid使用新id.
        List<AttachmentUpdateReqVO> attachmentUpdateReqList = BeanUtil.copyToList(oldSo.getAttachmentList(), AttachmentUpdateReqVO.class);
        attachmentUpdateReqList.forEach(t -> {
            t.setId(null);
            t.setReferenceId(so.getId());
        });
        attachmentService.createOrUpdateAndDeleteBatch(so.getId(), SoSiEnum.SO.getValue(), attachmentUpdateReqList, null);
        List<SoCargoUpdateReqVO> soCargoUpdateReqList = BeanUtil.copyToList(oldSo.getSoCargoList(), SoCargoUpdateReqVO.class);
        checkVesselOffHireMaintenanceDate(BeanUtil.copyProperties(soCargoUpdateReqList.get(0), SoCargoBaseVO.class), createReqVO.getSoVslvoyList().stream().map(e -> e.getVesselCode()).collect(Collectors.toList()));
        soCargoUpdateReqList.forEach(t -> {
            t.setId(null);
            t.setSoId(so.getId());
            if (SoCargoBaseVO.checkSpecialCargo(t.getCargoType(), t.getSubCargoType())) {
                createReqVO.setSpecialCargoFlag(true);
            }
        });
        soCargoService.createOrUpdateAndDeleteBatch(so.getId(), soCargoUpdateReqList, null);
        List<SoForecastUpdateReqVO> soForecastUpdateList = BeanUtil.copyToList(oldSo.getSoForecastList(), SoForecastUpdateReqVO.class);
        soForecastUpdateList.forEach(t -> {
            t.setId(null);
            t.setSoId(so.getId());
        });
        soForecastService.createOrUpdateAndDeleteBatch(so.getId(), soForecastUpdateList, null);
        List<SoContactUpdateReqVO> soContactUpdateList = BeanUtil.copyToList(oldSo.getSoContactList(), SoContactUpdateReqVO.class);
        soContactUpdateList.forEach(t -> {
            t.setId(null);
            t.setSoId(so.getId());
        });
        soContactService.createOrUpdateAndDeleteBatch(so.getId(), soContactUpdateList, null);
        SoScnUpdateReqVO soScnUpdateReqVO = BeanUtil.copyProperties(oldSo.getSoScn(), SoScnUpdateReqVO.class);
        soScnUpdateReqVO.setId(null);
        soScnUpdateReqVO.setSoId(so.getId());
        soScnService.createOrUpdateSo(so.getId(), soScnUpdateReqVO, null);
        List<RequirementsUpdateReqVO> requirementsUpdateList = BeanUtil.copyToList(oldSo.getRequirementsList(), RequirementsUpdateReqVO.class);
        requirementsUpdateList.forEach(t -> {
            t.setId(null);
            t.setReferenceId(so.getId());
        });
        requirementsService.createOrUpdateAndDeleteBatch(so.getId(), SoSiEnum.SO.getValue(), requirementsUpdateList, null);
        return so.getId();
    }

    public void checkUserLimit(String oldUserId, String currentUserId) {
/*        if (!Objects.equals(currentUserId, oldUserId)) {
            throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_UPDATE_LIMIT);
        }*/
    }

    public void checkSoNotNull(SoCreateAndUpdateReqVo createReqVO) {
        // @Valid 不支持嵌套，改手写
        if (createReqVO.getSo() == null) {
            Assertion.assertTrue(false).raise0(SoErrorCodeConstants.SO_CHECK_NULL.getCode(), "Basic Information");
        }
        if (CollectionUtil.isEmpty(createReqVO.getSoVslvoyList())) {
            Assertion.assertTrue(false).raise0(SoErrorCodeConstants.SO_CHECK_NULL.getCode(), "Voyage Route");
        }
        // 审核必须判断
        if (createReqVO.getCommitOrSave() == null || createReqVO.getCommitOrSave()) {
            if (createReqVO.getSoScn() == null) {
                Assertion.assertTrue(false).raise0(SoErrorCodeConstants.SO_CHECK_NULL.getCode(), "Shipper");
            }
            if (CollectionUtil.isEmpty(createReqVO.getSoForecastList())) {
                Assertion.assertTrue(false).raise0(SoErrorCodeConstants.SO_CHECK_NULL.getCode(), "Booking Forecast");
            }
            if (CollectionUtil.isEmpty(createReqVO.getSoCargoList())) {
                Assertion.assertTrue(false).raise0(SoErrorCodeConstants.SO_CHECK_NULL.getCode(), "Cargo");
            }
            if (CollectionUtil.isEmpty(createReqVO.getSoContactList())) {
                Assertion.assertTrue(false).raise0(SoErrorCodeConstants.SO_CHECK_NULL.getCode(), "Contact Info");
            }
            checkVesselOffHireMaintenanceDate(BeanUtil.copyProperties(createReqVO.getSoCargoList().get(0), SoCargoBaseVO.class), createReqVO.getSoVslvoyList().stream().map(e -> e.getVesselCode()).collect(Collectors.toList()));
        }
    }

    public void checkForbiddenCharacter(SoScnUpdateReqVO soScn, List<SoCargoUpdateReqVO> soCargoList) {
        //检查提单货描（英文品名）、唛头、收发通名称、地址、邮箱、电话、传真是否含有非英文字符。
        List<String> errorList = new ArrayList<>(16);
        if (CollectionUtil.isNotEmpty(soCargoList)) {
            soCargoList.forEach(e -> {
                CheckUtil.checkString(e.getCommodityEn(), errorList);
                CheckUtil.checkString(e.getMarks(), errorList);
            });
        }
        if (soScn != null) {
            CheckUtil.checkString(soScn.getCompanyName(), errorList);
            CheckUtil.checkString(soScn.getCompanyAddress(), errorList);
            CheckUtil.checkString(soScn.getEmail(), errorList);
            CheckUtil.checkString(soScn.getTelephone(), errorList);
            CheckUtil.checkString(soScn.getFax(), errorList);
        }
        if (CollectionUtil.isNotEmpty(errorList)) {
            Assertion.assertTrue(false).raise0(SoErrorCodeConstants.SO_CHECK_STR_ERROR.getCode(), StringUtils.join(errorList, ","));
        }
    }

    public void setDefaultTba(SoVslvoyUpdateReqVO vslvoy) {
        if (StringUtils.isEmpty(vslvoy.getVesselCode())) {
            vslvoy.setLaneCodeEx("TBA");
            vslvoy.setLaneCodeIm("TBA");
            vslvoy.setVoyageEx("TBA");
            vslvoy.setVoyageIm("TBA");
            vslvoy.setVesselCode("TBA");
        }
    }

    public void checkSoInfo(SoCreateAndUpdateReqVo createReqVO, boolean insertSpaceApproveStatusNull) {
        // 枚举值判断
        if (StringUtils.isNotEmpty(createReqVO.getSo().getCntrStatus()) && !CntrStatusEnum.getAllValue().contains(createReqVO.getSo().getCntrStatus())) {
            throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_CNTRSTATUS_ERROR);
        }
        if (StringUtils.isNotEmpty(createReqVO.getSo().getCntrOwner()) && !CntrOwnerEnum.getAllValue().contains(createReqVO.getSo().getCntrOwner())) {
            throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_CNTROWNER_ERROR);
        }
        // 新增修改时 业务校验 自动填充数据
        SoVslvoyUpdateReqVO vslvoyLineFirst = null;
        List<SoVslvoyUpdateReqVO> soVslvoyList = createReqVO.getSoVslvoyList();
        if (CollectionUtil.isNotEmpty(soVslvoyList)) {
            List<String> vesselCodeList = soVslvoyList.stream().map(e -> e.getVesselCode()).collect(Collectors.toList());
            Map<String, String> vesselOperatorMap = getVesselOperatorByCodes(vesselCodeList);
            StringBuilder routeDescription = new StringBuilder();
            for (int i = 0; i < soVslvoyList.size(); i++) {
                SoVslvoyUpdateReqVO temp = soVslvoyList.get(i);
                temp.setVesselOperator(vesselOperatorMap.get(temp.getVesselCode()));
                // 装货港与上一段卸货港要相同
                if (i - 1 >= 0) {
                    if (!soVslvoyList.get(i).getPolCode().equals(soVslvoyList.get(i - 1).getPodCode())) {
                        throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_LINER_POL_POD_CONFLICT);
                    }
                }
                //根据SO的Voyageroute 里的第一个liner段设置
                if (TransmodeEnum.LINER.getValue().equals(temp.getTransmode())) {
                    if (vslvoyLineFirst == null) {
                        vslvoyLineFirst = temp;
                    }
                } else {
                    // 多式联运设置默认值 TBA
                    setDefaultTba(temp);
                }
                temp.setRouteNo(i + 1);
                //批舱状态默认不更新
                if (insertSpaceApproveStatusNull == true) {
                    temp.setSpaceApproveStatus(null);
                }
                //拼接各个航段 Route：CNSHA-(LINER)-JYTPO-(LINER)-AUSYD
                if (i == 0) {
                    routeDescription.append(temp.getPolCode());
                    routeDescription.append("-(").append(temp.getTransmode()).append(")-");
                    routeDescription.append(temp.getPodCode());
                    createReqVO.getSo().setPorCode(temp.getPolCode());
                } else {
                    routeDescription.append("-(").append(temp.getTransmode()).append(")-");
                    routeDescription.append(temp.getPodCode());
                    if (i == soVslvoyList.size() - 1) {
                        createReqVO.getSo().setDelCode(temp.getPodCode());
                    }
                }
            }
            if (vslvoyLineFirst == null) {
                throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_NO_LINER);
            }
            // line 的第一个
            createReqVO.getSo().setPolCode(vslvoyLineFirst.getPolCode());
            createReqVO.getSo().setPodCode(vslvoyLineFirst.getPodCode());
            if (vslvoyLineFirst != null) {
                createReqVO.getSo().setBookingVessel(vslvoyLineFirst.getVesselCode());
                createReqVO.getSo().setBookingVoyage(vslvoyLineFirst.getVoyageEx());
                createReqVO.getSo().setCurrentPort(vslvoyLineFirst.getPolCode());
            }
            createReqVO.getSo().setRouteDescription(routeDescription.toString());
        }
        //45HC、20HC尺寸的箱子货类只能是AK
        boolean checkAk = false;
        List<String> checkAkList = Arrays.asList("45HC", "20HC");
        if (CollectionUtil.isNotEmpty(createReqVO.getSoForecastList())) {
            for (SoForecastUpdateReqVO forecast : createReqVO.getSoForecastList()) {
                if (checkAkList.contains(forecast.getCntrSize() + forecast.getCntrType())) {
                    checkAk = true;
                    break;
                }
            }
        }
        if (CollectionUtil.isNotEmpty(createReqVO.getSoCargoList())) {
            for (SoCargoUpdateReqVO e : createReqVO.getSoCargoList()) {
                if (checkAk && "AK".equals(e.getCargoType()) == false) {
                    throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_AK_ERROR);
                }
                //如果货类是DRY ，这里就需要再选择子类 是不是液袋货FLEXI、半开门ODO，
                if (CARGO_TYPE_DRY.equals(e.getCargoType()) && StringUtils.isNotEmpty(e.getSubCargoType())) {
                    if (!SubCargoTypeEnum.getAllValue().contains(e.getSubCargoType())) {
                        throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_CARGO_TYPE_DRY);
                    }
                }
                //货类是RD、RF温度和通风度是必填的 并且是阻断的
                if (Arrays.asList(CargoTypeEnum.RD.getValue(), CargoTypeEnum.RF.getValue()).contains(e.getCargoType())) {
                    if (StringUtils.isEmpty(e.getTemperature()) || StringUtils.isEmpty(e.getVantilation())){
                        throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_RD_RF_ERROR);
                    }
                }
            }
        }
        //箱主SNL 订舱要求里有重去空回（这个是 SO 新增和提交的校验）
        if (Objects.equals(CntrOwnerEnum.SNL.getValue(), createReqVO.getSo().getCntrOwner())) {
            if (CollectionUtil.isNotEmpty(createReqVO.getRequirementsList()) && createReqVO.getRequirementsList().stream().filter(e -> Objects.equals(ROUND_TRIP, e.getRequireCode())).count() > 0) {
                throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_BOOKING_ROUND_TRIP_ERROR);
            }
        }
    }

    public void checkPhoneAndEmail(SoCreateAndUpdateReqVo createReqVO) {
        if (StringUtils.isNotEmpty(createReqVO.getSo().getSoNotifyEmails())) {
            String[] emails = createReqVO.getSo().getSoNotifyEmails().split(";");
            checkEmail(emails, "so通知");
        }
        if (StringUtils.isNotEmpty(createReqVO.getSo().getSiNotifyEmails())) {
            String[] emails = createReqVO.getSo().getSiNotifyEmails().split(";");
            checkEmail(emails, "si通知");
        }
        if (createReqVO.getSoScn() != null) {
            checkEmail(createReqVO.getSoScn().getEmail(), "相关方");
        }
        if (CollectionUtil.isNotEmpty(createReqVO.getSoContactList())) {
            createReqVO.getSoContactList().forEach(e -> {
                checkEmail(e.getContactEmail(), "联系人");
            });
        }
    }

    public void checkEmail(String email, String key) {
        if (StringUtils.isNotEmpty(email) && !Validator.isEmail(email)) {
            throw ServiceExceptionUtil.exception(new ErrorCode(SoErrorCodeConstants.SO_EMAIL_ERROR.getCode(), key + email + SoErrorCodeConstants.SO_EMAIL_ERROR.getMsg()));
        }
    }

    public void checkEmail(String[] emails, String key) {
        for (String email : emails) {
            if (!Validator.isEmail(email)) {
                throw ServiceExceptionUtil.exception(new ErrorCode(SoErrorCodeConstants.SO_EMAIL_ERROR.getCode(), key + email + SoErrorCodeConstants.SO_EMAIL_ERROR.getMsg()));
            }
        }
    }

    public void checkPhone(String phone, String key) {
        if (StringUtils.isNotEmpty(phone) && !Validator.isMobile(phone)) {
            throw ServiceExceptionUtil.exception(new ErrorCode(SoErrorCodeConstants.SO_PHONE_ERROR.getCode(), key + phone + SoErrorCodeConstants.SO_PHONE_ERROR.getMsg()));
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @Lock4j(name = "soUnique", keys = {"'batchDeleteDraft'", "#userId"})
    public void batchDeleteDraft(List<Long> ids, String userId) {
        soService.deleteBySoIds(ids, userId);
        soVslvoyService.deleteBySoIds(ids);
        soCargoService.deleteBySoIds(ids);
        soForecastService.deleteBySoIds(ids);
        soContactService.deleteBySoIds(ids);
        soScnService.deleteBySoIds(ids);
        attachmentService.deleteByReferenceIds(ids, SoSiEnum.SO.getValue());
        requirementsService.deleteByReferenceIds(ids, SoSiEnum.SO.getValue());
    }

    @Override
    public SoDetailRespVO getByBookingNoOrBlNo(SoQueryReqVO req) {
        SoDO soDo = soService.getSoDoByBookingNoOrBlNo(req);
        SoDetailRespVO detail = getDetail(soDo.getId(), StringUtils.EMPTY, false);
        boolean isCnngbMulti = CNNGB.equals(soDo.getPolCode()) && !CNNGB.equals(soDo.getPorCode()) &&
                configuration.getTopAgent().contains(soDo.getTopAgent());

        if (NORTHERN_THREE_PORTS.contains(soDo.getPolCode())
                || (CNSHA.equals(soDo.getPolCode()) && !CNSHA.equals(soDo.getPorCode()))
                || isCnngbMulti){
            ManifestInfoRespVO manifestDetailByBookingNo = manifestService.getManifestDetailByBookingNo(soDo.getBookingNo());
            if (!ObjectUtils.isEmpty(manifestDetailByBookingNo)){

                SoDetailRespVO soDetailRespVO = new SoDetailRespVO();

                ManifestRespVO manifest = manifestDetailByBookingNo.getManifest();
                if(!ObjectUtils.isEmpty(manifest)){
                    if (!ConfirmStatusEnum.Y.getValue().equals(manifest.getConfirmStatus())){
                        throw ServiceExceptionUtil.exception(ManifestErrorCodeConstants.MANIFEST_NOT_CONFIRM);
                    }
                    //构造soPayment数据
                    SoPaymentRespVO soPaymentRespVO = new SoPaymentRespVO();
                    soPaymentRespVO.setDefaultPayment(manifest.getPayment());
                    soPaymentRespVO.setDesignatedPayment(manifest.getPayment());
                    soPaymentRespVO.setChargeCode("FRT");
                    soDetailRespVO.setSoPaymentList(Arrays.asList(soPaymentRespVO));

                    SoRespVO soRespVO = BeanUtil.copyProperties(manifest, SoRespVO.class);
                    if (ObjectUtils.isEmpty(soRespVO.getRouteId())){
                        soRespVO.setRouteId(soDo.getRouteId());
                    }
                    if (ObjectUtils.isEmpty(soRespVO.getAgreementNo())){
                        soRespVO.setAgreementNo(soDo.getAgreementNo());
                    }
                    soRespVO.setBlNo(manifest.getManifestNo());
                    soDetailRespVO.setSo(soRespVO);
                }
                List<ManifestCargoRespVO> manifestCargoList = manifestDetailByBookingNo.getManifestCargoList();
                if(!ObjectUtils.isEmpty(manifestCargoList)){
                    List<SoCargoRespVO> soCargoRespVos = BeanUtil.copyToList(manifestCargoList, SoCargoRespVO.class);
                    soDetailRespVO.setSoCargoList(soCargoRespVos);
                }
                //箱信息
                List<ManifestCntrRespVO> manifestCntrList = manifestDetailByBookingNo.getManifestCntrList();
                soDetailRespVO.setManifestCntrList(ObjectUtils.isEmpty(manifestCntrList)?new ArrayList<>():manifestCntrList);

                List<SoForecastRespVO> soForecastList = manifestDetailByBookingNo.getSoForecastList();
                if(!ObjectUtils.isEmpty(soForecastList)){
                    List<SoForecastRespVO> soForecastRespVos = BeanUtil.copyToList(soForecastList, SoForecastRespVO.class);
                    soDetailRespVO.setSoForecastList(soForecastRespVos);
                }
                List<ManifestVslvoyRespVO> manifestVslvoyList = manifestDetailByBookingNo.getManifestVslvoyList();
                if(!ObjectUtils.isEmpty(manifestVslvoyList)){
                    List<SoVslvoyRespVO> soVslvoyRespVos = BeanUtil.copyToList(manifestVslvoyList, SoVslvoyRespVO.class);
                    soDetailRespVO.setSoVslvoyList(soVslvoyRespVos);
                }
                //相关方
                List<ManifestScnRespVO> manifestScnList = manifestDetailByBookingNo.getManifestScnList();
                if(!ObjectUtils.isEmpty(manifestScnList)){
                    ManifestScnRespVO manifestScnRespVO = manifestScnList.stream().filter(t -> PartyTypeCodeEnum.SHIPPER.getValue().equals(t.getPartyTypeCode())).findFirst().orElse(null);
                    SoScnRespVO soScnRespVO = BeanUtil.copyProperties(manifestScnRespVO, SoScnRespVO.class);
                    soDetailRespVO.setSoScn(soScnRespVO);
                    soDetailRespVO.setManifestScnList(manifestDetailByBookingNo.getManifestScnList());
                }else {
                    soDetailRespVO.setManifestScnList(new ArrayList<>());
                }

                //舱单没有的数据，置空列表
                soDetailRespVO.setAttachmentList(detail.getAttachmentList());
                soDetailRespVO.setBcrCheckList(new ArrayList<>());
                soDetailRespVO.setSoContactList(detail.getSoContactList());
                soDetailRespVO.setRequirementsList(detail.getRequirementsList());
                return soDetailRespVO;
            }else {
                throw ServiceExceptionUtil.exception(ManifestErrorCodeConstants.MANIFEST_NOT_EXISTS);
            }
        }else {
            return detail;
        }

    }

    @Override
    public List<SoDetailRespVO> getDetailList(List<Long> ids) {
        List<SoDetailRespVO> detailList = new ArrayList<>(8);
        if (CollectionUtil.isEmpty(ids)) {
            return detailList;
        }
        ids.forEach(id -> {
            detailList.add(getDetail(id, StringUtils.EMPTY, false));
        });
        return detailList;
    }

    @Override
    public SoDetailRespVO getDetail(Long id) {
        return getDetail(id, StringUtils.EMPTY, false);
    }

    public void setVslvoyTerminal(List<SoVslvoyRespVO> soVslvoyList) {
        if (CollectionUtil.isNotEmpty(soVslvoyList)) {
            soVslvoyList.forEach(e -> {
                getVesselVoyTerminal(e);
            });
        }
    }

    public void setVslvoyTerminalCreate(List<SoVslvoyCreateReqVO> soVslvoyList) {
        if (CollectionUtil.isNotEmpty(soVslvoyList)) {
            soVslvoyList.forEach(e -> {
                getVesselVoyTerminal(e);
            });
        }
    }

    public void getVesselVoyTerminal(SoVslvoyBaseVO e) {
        Map<String, VesselVoyRespDTO> terminalMap = costChargeUtil.getVesselVoyMap(e.getLaneCodeEx(), e.getVoyageEx(), e.getVesselCode());
        if (terminalMap != null) {
            VesselVoyRespDTO voyPol = terminalMap.get(e.getPolCode());
            if (voyPol != null) {
                e.setPolTerminal(voyPol.getTerminalCode());
                e.setPolEtd(voyPol.getEtd());
                // etd未维护，取默认值 ptd
                if (StringUtils.isEmpty(e.getPolEtd())) {
                    e.setPolEtd(voyPol.getPtd());
                }
            }
            VesselVoyRespDTO voyPod = terminalMap.get(e.getPodCode());
            if (voyPod != null) {
                e.setPodTerminal(voyPod.getTerminalCode());
                e.setPodEta(voyPod.getEta());
                // eta未维护，取默认值 pta
                if (StringUtils.isEmpty(e.getPodEta())) {
                    e.setPodEta(voyPod.getPta());
                }
            }
        }
    }

    public SoDetailRespVO getDetail(Long id, String userId, boolean needPreviousNext) {
        SoDetailRespVO detail = new SoDetailRespVO();
        SoDO sodo = soService.getSo(id);
        if (sodo == null) {
            throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_NOT_EXISTS);
        }
/*        if (StringUtils.isNotEmpty(userId) && !userId.equals(sodo.getCreator())) {
            throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_BOOKING_MATCH_USER_ERROR);
        }*/
        // 是否需要查询上下数据 无效
        if (needPreviousNext) {
            detail.setPreviousSo(soService.findPreviousOrNext(id, SoServiceImpl.PREVIOUS));
            detail.setNextSo(soService.findPreviousOrNext(id, SoServiceImpl.NEXT));
        }
        SpecialCargoApplyDO specialCargoApply = soCommonService.getSpecialCargoApplyBySoId(sodo.getId(), sodo.getBookingNo());
        detail.setSo(SoConvert.INSTANCE.convert(sodo));
        if (specialCargoApply != null) {
            detail.getSo().setSpecialCargoAuditStatus(specialCargoApply.getApplyStatus());
            detail.getSo().setSpecialCargoAuditId(specialCargoApply.getId());
        }
        detail.setSoVslvoyList(SoVslvoyConvert.INSTANCE.convertList(soVslvoyService.seleteBySoId(id)));
        detail.setSoCargoList(SoCargoConvert.INSTANCE.convertList(soCargoService.seleteBySoId(id)));
        detail.setSoForecastList(SoForecastConvert.INSTANCE.convertList(soForecastService.seleteBySoId(id)));
        detail.setSoContactList(SoContactConvert.INSTANCE.convertList(soContactService.seleteBySoId(id)));
        detail.setSoScn(SoScnConvert.INSTANCE.convert(soScnService.seleteBySoId(id)));
        detail.setAttachmentList(AttachmentConvert.INSTANCE.convertList(attachmentService.seleteByReferenceId(id, SoSiEnum.SO.getValue())));
        detail.setRequirementsList(RequirementsConvert.INSTANCE.convertList(requirementsService.seleteByReferenceId(id, SoSiEnum.SO.getValue())));
        detail.setBcrCheckList(soBcrCheckService.selectBySoId(id));
        detail.setOperateLogList(operateLogService.selectByBusinessId(SoSiEnum.SO.getValue(), id));
        setVslvoyTerminal(detail.getSoVslvoyList());
        setSoVslvoyFirstLinerAndEtd(detail.getSoVslvoyList(), detail.getSo());
        return detail;
    }

    @Override
    public List<SoDetailRespVO> getSoDetailListByBlNos(List<String> blNos) {
        List<SoDO> soListByBlNos = soService.getSoListByBlNos(blNos);
        if (CollectionUtil.isNotEmpty(soListByBlNos)){
            List<Long> soIdList = soListByBlNos.stream().map(SoDO::getId).collect(Collectors.toList());
            Map<Long, List<SoVslvoyDO>> longSoVslvoyDoMap = soVslvoyService.seleteBySoIdsMap(soIdList);
            List<SoForecastDO> soForecastDos = soForecastService.seleteBySoIdList(soIdList);
            Map<Long, List<SoForecastDO>> forecastListMap = Optional.ofNullable(soForecastDos).orElse(Collections.emptyList()).stream().collect(Collectors.groupingBy(SoForecastDO::getSoId));
            Map<Long, SoCargoDO> longSoCargoDoMap = soCargoService.selectBySoIds(soIdList);
            Map<Long, SoScnDO> longSoScnDoMap = soScnService.selectBySoIds(soIdList);

            ArrayList<SoDetailRespVO> soDetailRespVos = new ArrayList<>();
            List<SoRespVO> soRespVos = BeanUtil.copyToList(soListByBlNos, SoRespVO.class);
            soRespVos.stream().forEach(t -> {
                SoDetailRespVO soDetailRespVo = new SoDetailRespVO();
                soDetailRespVo.setSo(t);

                if (CollectionUtil.isNotEmpty(longSoVslvoyDoMap)){
                    soDetailRespVo.setSoVslvoyList(BeanUtil.copyToList(longSoVslvoyDoMap.get(t.getId()),SoVslvoyRespVO.class));
                }

                if (CollectionUtil.isNotEmpty(forecastListMap)){
                    soDetailRespVo.setSoForecastList(BeanUtil.copyToList(forecastListMap.get(t.getId()),SoForecastRespVO.class));
                }

                if (CollectionUtil.isNotEmpty(longSoCargoDoMap)){
                    soDetailRespVo.setSoCargoList(Arrays.asList(BeanUtil.copyProperties(longSoCargoDoMap.get(t.getId()),SoCargoRespVO.class)));
                }

                if (CollectionUtil.isNotEmpty(longSoScnDoMap)){
                    soDetailRespVo.setSoScn(BeanUtil.copyProperties(forecastListMap.get(t.getId()),SoScnRespVO.class));
                }

                soDetailRespVos.add(soDetailRespVo);
            });

            return soDetailRespVos;
        }
        return Collections.emptyList();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @Lock4j(name = "soUnique", keys = {"'batchCopy'", "#userId"})
    public void batchCopy(List<Long> ids, String userId) {
        for (Long id : ids) {
            copyDetail(id, userId, true);
        }
    }

    @Override
    public SoCopyDetailRespVo copyDetail(Long id, String userId, boolean needInsert) {
        SoCopyDetailRespVo newCopySo = new SoCopyDetailRespVo();
        SoDO oldSo = soService.getSo(id);
        if (oldSo == null || !Objects.equals(oldSo.getCreator(), userId)) {
            throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_NOT_EXISTS);
        }
        // 设置默认值 绑定旧的ID
        soService.setDefaultDraftValue(oldSo);
        newCopySo.setOldId(id);
        SoCreateReqVO so = SoConvert.INSTANCE.convertCreate(oldSo);
        List<SoVslvoyCreateReqVO> soVslvoyList = SoVslvoyConvert.INSTANCE.convertCreateList(soVslvoyService.seleteBySoId(id));
        setVslvoyTerminalCreate(soVslvoyList);
        List<SoCargoCreateReqVO> soCargoList = SoCargoConvert.INSTANCE.convertCreateList(soCargoService.seleteBySoId(id));
        List<SoForecastCreateReqVO> soForecastList = SoForecastConvert.INSTANCE.convertCreateList(soForecastService.seleteBySoId(id));
        List<SoContactCreateReqVO> soContactList = SoContactConvert.INSTANCE.convertCreateList(soContactService.seleteBySoId(id));
        SoScnCreateReqVO soScn = SoScnConvert.INSTANCE.convertCreate(soScnService.seleteBySoId(id));
        List<AttachmentCreateReqVO> attachmentList = AttachmentConvert.INSTANCE.convertCreateList(attachmentService.seleteByReferenceId(id, SoSiEnum.SO.getValue()));
        List<RequirementsCreateReqVO> requirementsList = RequirementsConvert.INSTANCE.convertCreateList(requirementsService.seleteByReferenceId(id, SoSiEnum.SO.getValue()));
        //清空soId绑定关系 状态清空
        soVslvoyList.forEach(e -> {
            e.setSoId(null);
            e.setStatusNull();
        });
        soCargoList.forEach(e -> e.setSoId(null));
        soForecastList.forEach(e -> e.setSoId(null));
        soContactList.forEach(e -> e.setSoId(null));
        attachmentList.forEach(e -> {
            e.setReferenceId(null);
            e.setReferenceType(null);
        });
        requirementsList.forEach(e -> {
            e.setReferenceId(null);
            e.setReferenceType(null);
        });
        if (soScn != null) {
            soScn.setSoId(null);
        }
        if (!needInsert) {
            newCopySo.setSo(so);
            newCopySo.setSoVslvoyList(soVslvoyList);
            newCopySo.setSoCargoList(soCargoList);
            newCopySo.setSoForecastList(soForecastList);
            newCopySo.setSoContactList(soContactList);
            newCopySo.setSoScn(soScn);
            newCopySo.setAttachmentList(attachmentList);
            newCopySo.setRequirementsList(requirementsList);
        } else {
            //重新设置soId
            SoDO newSo = soService.createSo(so);
            soVslvoyList.forEach(e -> e.setSoId(newSo.getId()));
            soVslvoyService.createBatch(soVslvoyList);
            soCargoList.forEach(e -> e.setSoId(newSo.getId()));
            soCargoService.createBatch(soCargoList);
            soForecastList.forEach(e -> e.setSoId(newSo.getId()));
            soForecastService.createBatch(soForecastList);
            soContactList.forEach(e -> e.setSoId(newSo.getId()));
            soContactService.createBatch(soContactList);
            attachmentList.forEach(e -> {
                e.setReferenceId(newSo.getId());
                e.setReferenceType(SoSiEnum.SO.getValue());
            });
            attachmentService.createBatch(attachmentList);
            requirementsList.forEach(e -> {
                e.setReferenceId(newSo.getId());
                e.setReferenceType(SoSiEnum.SO.getValue());
            });
            requirementsService.createBatch(requirementsList);
            if (soScn != null) {
                soScn.setSoId(newSo.getId());
                soScnService.createSoScn(soScn);
            }
        }
        return newCopySo;
    }

    @Override
    public PageResult<SoRespVO> getSoPage(SoPageReqVO pageVO) {
        return getSoPage(pageVO, false);
    }

    @Override
    public List<SoRespVO> queryList(SoListReqVO reqVO) {
        if (CollectionUtil.isEmpty(reqVO.getBookingNoList())) {
            Assertion.assertTrue(false).raise0(SoErrorCodeConstants.SO_CHECK_NULL.getCode(), "Booking No");
        }
        List<SoRespVO> results = SoConvert.INSTANCE.convertList(soService.queryList(reqVO));
        addOtherSum(results);
        return results;
    }
    @Override
    public PageResult<SoRespVO> getSoPage(SoPageReqVO pageVO, boolean approve) {
        //查询主要信息
        PageResult<SoDO> pageResult = soService.getSoPage(pageVO);
        List<SoRespVO> results = SoConvert.INSTANCE.convertList(pageResult.getList());
        // 拼装其他汇总信息
        if (approve) {
            addOtherSum(results, true, pageVO.getSpaceStatusList());
        } else {
            addOtherSum(results);
        }

        return new PageResult<>(results, pageResult.getTotal());
    }

    @Override
    public PageResult<SoRespVO> getSoPageAmend(SoPageReqVO pageVO) {
        //查询主要信息
        PageResult<SoRespVO> pageResult = soService.getSoPageAmend(pageVO);
        // 拼装其他汇总信息
        addOtherSum(pageResult.getList());
        return pageResult;
    }

    /**
     * addOtherSum
     *
     * @param results SoRespVO
     */
    public void addOtherSum(List<SoRespVO> results) {
        addOtherSum(results, false, null);
    }

    /**
     * addOtherSum
     *
     * @param results SoRespVO
     * @param approve 是否批舱查询
     */
    public void addOtherSum(List<SoRespVO> results, boolean approve, String[] spaceStatusList) {
        if (CollectionUtil.isNotEmpty(results)) {
            List<Long> soIds = results.stream().map(e -> e.getId()).collect(Collectors.toList());
            List<String> bookingNos = results.stream().map(e -> e.getBookingNo()).collect(Collectors.toList());
            List<String> blNoList = results.stream().map(e -> e.getBlNo()).collect(Collectors.toList());
            Map<String, List<BlnoDO>> blnoMap = blnoService.findBlnoByBlNos(blNoList);
            //订舱人编码转英文名
            List<String> collect = results.stream().map(SoRespVO::getBookingParty).collect(Collectors.toList());
            Map<Long, SpecialCargoApplyDO> specialCargoApplyMap = soCommonService.getSpecialCargoApplyBySoIds(soIds);
            Map<String, SpecialCargoApplyDO> specialCargoApplyBookingNoMap = soCommonService.getSpecialCargoApplyByBookingNoMap(bookingNos);
            Map<Long, AttachmentCountVO> attachmentSumMap = attachmentService.selectSumByReferenceIds(soIds, SoSiEnum.SO.getValue()).stream().collect(Collectors.toMap(AttachmentCountVO::getReferenceId, e -> e));
            Map<Long, SoCargoSumVO> cargoSumMap = soCargoService.selectSumBySoIds(soIds).stream().collect(Collectors.toMap(SoCargoSumVO::getSoId, e -> e));
            Map<Long, SoForecastSumVO> forecastSumMap = soForecastService.selectSumBySoIds(soIds).stream().collect(Collectors.toMap(SoForecastSumVO::getSoId, e -> e));
            Map<Long, SoCargoDO> soCargoMap = soCargoService.selectBySoIds(soIds);
            Map<String, String> companyNameMap = nameTransferService.getCompanyLocalNameMap(collect);
            Map<Long, List<SoVslvoyDO>> vslvoyLinerMap;
            if (approve) {
                vslvoyLinerMap = soVslvoyService.seleteBySoIdsMap(soIds, spaceStatusList);
            } else {
                vslvoyLinerMap = soVslvoyService.seleteBySoIdsMap(soIds);
            }
            results.forEach(e -> {
                SpecialCargoApplyDO specialCargoApply = specialCargoApplyMap.get(e.getId());
                SpecialCargoApplyDO specialCargoApplyOther = specialCargoApplyBookingNoMap.get(e.getBookingNo());
                if (specialCargoApply == null) {
                    specialCargoApply = specialCargoApplyOther;
                }
                if (specialCargoApply != null) {
                    e.setSpecialCargoAuditStatus(specialCargoApply.getApplyStatus());
                    e.setSpecialCargoAuditId(specialCargoApply.getId());
                } else {
                    SoCargoDO cargo = soCargoMap.get(e.getId());
                    if (cargo != null) {
                        if (SoCargoBaseVO.checkSpecialCargo(cargo.getCargoType(), cargo.getSubCargoType())) {
                            // 需要特种货审批
                            e.setSpecialCargoAuditStatus("Need Special Cargo Audit");
                        }
                    }
                }
                AttachmentCountVO attachmentCount = attachmentSumMap.get(e.getId());
                if (attachmentCount != null) {
                    e.setAttachmentCount(attachmentCount.getAttachmentCount());
                }
                SoCargoSumVO cargoSum = cargoSumMap.get(e.getId());
                if (cargoSum != null) {
                    e.setCargoTypeSum(cargoSum.getCargoTypeSum());
                    e.setGrossWeightSum(cargoSum.getGrossWeightSum());
                }
                SoForecastSumVO forecastSum = forecastSumMap.get(e.getId());
                if (forecastSum != null) {
                    e.setForecastDesc(forecastSum.getForecastDesc());
                }
                List<SoVslvoyDO> soVslvoyList = vslvoyLinerMap.get(e.getId());
                if (soVslvoyList != null) {
                    e.setSoVslvoyList(SoVslvoyConvert.INSTANCE.convertList(soVslvoyList));
                    setVslvoyTerminal(e.getSoVslvoyList());
                    setSoVslvoyFirstLinerAndEtd(e.getSoVslvoyList(), e);
                }
                e.setBookingPartyEn(companyNameMap.get(e.getBookingParty()));

                if (CollectionUtil.isNotEmpty(blnoMap.get(e.getBlNo()))) {
                    e.setCreateSi("Y");
                } else {
                    e.setCreateSi("N");
                }
            });
        }
    }

    public void setSoVslvoyFirstLinerAndEtd(List<SoVslvoyRespVO> soVslvoyList, SoRespVO e) {
        if (CollectionUtil.isEmpty(soVslvoyList)) {
            return;
        }
        soVslvoyList.forEach(t -> {
            if (TransmodeEnum.LINER.getValue().equals(t.getTransmode())) {
                if (e.getSoVslvoyFirstLiner() == null) {
                    e.setPolEtd(t.getPolEtd());
                    e.setSoVslvoyFirstLiner(t);
                }
                //最后Liner
                e.setPodEta(t.getPodEta());
            }
        });
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @Lock4j(name = "soUnique", keys = {"'batchSubmitDraft'", "#userId"})
    public void batchSubmitDraft(List<Long> ids, String userId) {
        List<SoDO> solist = soService.checkSoStatusByUserId(ids, Arrays.asList(SoStatusEnum.BookingDraft.getValue(), SoStatusEnum.AwaitingMaterialUploaded.getValue()), userId);
        List<SoDO> soDraftlist = solist.stream().filter(e -> SoStatusEnum.BookingDraft.getValue().equals(e.getSoStatus())).collect(Collectors.toList());
        List<String> bkpCodes = soDraftlist.stream().map(e -> e.getBookingParty()).collect(Collectors.toList());
        List<String> currentPols = soDraftlist.stream().map(e -> e.getCurrentPort()).collect(Collectors.toList());
        Map<String, String> bkpCodesMap = blnoRuleKlcodeService.findRuleByBkpCodeAndCreate(bkpCodes, true);
        Map<String, String> currentPolsMap = blnoRulePbcodeService.findRuleByPorCodes(currentPols);
        String currentYear = String.valueOf(LocalDate.now().getYear());
        BlnoRuleYearDO yearDO = blnoRuleYearService.selectBlnoRuleYearOne(currentYear);
        if (yearDO == null) {
            throw ServiceExceptionUtil.exception(new ErrorCode(SoErrorCodeConstants.SO_BLNORULE_YEAR_ERROR.getCode(), currentYear + SoErrorCodeConstants.SO_BLNORULE_YEAR_ERROR.getMsg()));
        }
        //订特种货时，不允许提交禁接的船舶
        checkMaintenanceDateBySoIds(ids);
        solist.forEach(e -> {
            if (SoStatusEnum.BookingDraft.getValue().equals(e.getSoStatus())) {
                // 根据规则生成订舱号，提单号
                addBookingNoAndBlNo(e, bkpCodesMap, currentPolsMap, yearDO.getYearBlnoCode());
                e.setSoStatus(SoStatusEnum.BookingProcessing.getValue());
                e.setBookingTime(new Date(System.currentTimeMillis()));
            } else {
                // 客户已补充材料
                e.setSoStatus(SoStatusEnum.CustomerUploadedMaterial.getValue());
            }
            //提交后更新 订舱时间
            e.setSoContentStatus(SoContentStatusEnum.PENDING.getValue());
            e.setUpdater(userId);
            e.setUpdateTime(LocalDateTime.now());
        });
        //批量跟新更新状态
        soVslvoyService.submitDraftSetDefault(ids);
        soService.updateBatch(solist);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void getBookingNoAndBlNoBySo(SoDO so) {
        Map<String, String> bkpCodesMap = blnoRuleKlcodeService.findRuleByBkpCodeAndCreate(Arrays.asList(so.getBookingParty()), false);
        Map<String, String> currentPolsMap = blnoRulePbcodeService.findRuleByPorCodes(Arrays.asList(so.getCurrentPort()));
        String currentYear = String.valueOf(LocalDate.now().getYear());
        BlnoRuleYearDO yearDO = blnoRuleYearService.selectBlnoRuleYearOne(currentYear);
        if (yearDO == null) {
            throw ServiceExceptionUtil.exception(new ErrorCode(SoErrorCodeConstants.SO_BLNORULE_YEAR_ERROR.getCode(), currentYear + SoErrorCodeConstants.SO_BLNORULE_YEAR_ERROR.getMsg()));
        }
        addBookingNoAndBlNo(so, bkpCodesMap, currentPolsMap, yearDO.getYearBlnoCode());
    }

    public void addBookingNoAndBlNo(SoDO so, Map<String, String> bkpCodesMap, Map<String, String> currentPolsMap, String yearCode) {
        if (StringUtils.isNotEmpty(so.getBookingNo())) {
            return;
        }
        if (bkpCodesMap.get(so.getBookingParty()) == null) {
            throw ServiceExceptionUtil.exception(new ErrorCode(SoErrorCodeConstants.SO_BLNORULE_BKPCODE_ERROR.getCode(), so.getBookingParty() + SoErrorCodeConstants.SO_BLNORULE_BKPCODE_ERROR.getMsg()));
        }
        if (currentPolsMap.get(so.getCurrentPort()) == null) {
            throw ServiceExceptionUtil.exception(new ErrorCode(SoErrorCodeConstants.SO_BLNORULE_CURRENTPOL_ERROR.getCode(), so.getCurrentPort() + SoErrorCodeConstants.SO_BLNORULE_CURRENTPOL_ERROR.getMsg()));
        }
        //订舱号规则：起运地（2）+订舱人（3）+序号（6）
        String bookingNo = currentPolsMap.get(so.getCurrentPort()) + bkpCodesMap.get(so.getBookingParty());
        so.setBookingNo(bookingNo + getIncByRedis(bookingNo));
        //提单号规则：箱主（3）+年份（1）+起运地（2）+订舱人（3）+序号（6）
        so.setBlNo(so.getCntrOwner() + yearCode + so.getBookingNo());
    }

    public String getIncByRedis(String bookingNo) {
        RAtomicLong rAtomicLong = redissonClient.getAtomicLong(REDIS_SO_BOOKING_NO + bookingNo);
        long num = rAtomicLong.incrementAndGet();
        if (num > MAX_NUM) {
            //序号（6）
            throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_CREATE_BOOKINGNO_ERROR);
        }
        return String.format("%06d", num);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @Lock4j(name = "soUnique", keys = {"'batchReturnToDraft'", "#userId"})
    public void batchReturnToDraft(Collection<Long> ids, String userId) {
        List<SoDO> soList = soService.checkSoStatusByUserId(ids, Arrays.asList(SoStatusEnum.BookingProcessing.getValue(), SoStatusEnum.CustomerUploadedMaterial.getValue()), userId);
        Map<Long, SpecialCargoApplyDO> specialCargoApplyMap = soCommonService.getSpecialCargoApplyBySoIds(new ArrayList<Long>(ids));
        //特种货申请没有审核，则允许退回草稿。同时特种货申请记录也要退回草稿
        List<String> errorList = new ArrayList<>(16);
        List<Long> specialCargoApplyIds = new ArrayList<>(16);
        for (SoDO so : soList) {
            SpecialCargoApplyDO specialCargoApply = specialCargoApplyMap.get(so.getId());
            if (specialCargoApply != null) {
                if (!ApplyStatusEnum.PENDING.getValue().equals(specialCargoApply.getApplyStatus())) {
                    //有特种货申请审核，不能退回成草稿
                    errorList.add(so.getBookingNo());
                } else {
                    specialCargoApplyIds.add(specialCargoApply.getId());
                }
            }
        }
        if (CollectionUtil.isNotEmpty(errorList)) {
            Assertion.assertTrue(false).raise0(SoErrorCodeConstants.SO_RETUAN_DRAFT_ERROR.getCode(), StringUtils.join(errorList, ","));
        }
        if (CollectionUtil.isNotEmpty(specialCargoApplyIds)) {
            specialCargoApplyService.returnEdit(specialCargoApplyIds);
        }
        //批量跟新更新状态
        //返回草稿 不需要清空 订单号和提单号
        soService.batchReturnToDraft(ids);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @Lock4j(name = "soUnique", keys = {"'batchCancelBooking'", "#userId"})
    public CommonResult<Void> batchCancelBooking(Collection<Long> ids, String userId) {
        List<String> soStatusCancel = Arrays.asList(SoStatusEnum.BookingProcessing.getValue(), SoStatusEnum.BookingPendingSpace.getValue(),
                SoStatusEnum.AwaitingMaterialUploaded.getValue(), SoStatusEnum.CustomerUploadedMaterial.getValue());
        List<SoDO> solist = soService.checkSoStatusByUserId(ids, soStatusCancel, userId);
        solist.forEach(e -> {
            e.setSoStatus(SoStatusEnum.BookingCancelledByCustomer.getValue());
            e.setUpdater(userId);
            e.setUpdateTime(LocalDateTime.now());
        });
        //批量跟新更新状态
        soService.updateBatch(solist);
        solist.forEach(e -> {
            soCommonService.syncSoStatus(e.getId(), SoStatusEnum.BookingCancelledByCustomer.getValue());
        });
        return CommonResult.success();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @Lock4j(name = "soUnique", keys = {"'batchChangeBKGVessel'", "#userId"})
    public void batchChangeBkgVessel(SoVslvoyUpdateReqVO updateReqVO, String userId) {
        // 数据类型校验
        if (CollectionUtil.isEmpty(updateReqVO.getSoIdList())) {
            throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_NOT_EXISTS);
        }
        if (!TransmodeEnum.LINER.getValue().equals(updateReqVO.getTransmode())) {
            throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_CHANGE_BKG_VESSEL_ERROR);
        }
        List<String> soStatusChange = Arrays.asList(SoStatusEnum.BookingPendingSpace.getValue(), SoStatusEnum.BookingConfirmed.getValue());
        List<SoDO> soList = soService.checkSoStatusByUserId(updateReqVO.getSoIdList(), soStatusChange, userId);
        List<SoVslvoyDO> soVslvoyList = soVslvoyService.selectFirstLinerBySoIds(updateReqVO.getSoIdList());
        // polCode podCode 一致性校验
        long errorPolPod = soVslvoyList.stream().filter(e -> !Objects.equals(updateReqVO.getPolCode(), e.getPolCode()) || !Objects.equals(updateReqVO.getPodCode(), e.getPodCode())).count();
        if (errorPolPod > 0) {
            throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_CHANGE_BKG_VESSEL_NO_MATCH_ERROR);
        }
        if (Objects.equals(updateReqVO.getVesselCode(), soList.get(0).getBookingVessel()) && Objects.equals(updateReqVO.getVoyageEx(), soList.get(0).getBookingVessel())) {
            throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_CHANGE_BKG_VESSEL_SAME_ERROR);
        }
        // 船名航次截至时间 规则查询
        checkVslvoy(Arrays.asList(updateReqVO));
        Map<String, String> vesselOperatorMap = getVesselOperatorByCodes(Arrays.asList(updateReqVO.getVesselCode()));
        updateReqVO.setVesselOperator(vesselOperatorMap.get(updateReqVO.getVesselCode()));
        List<Long> returnIdList = new ArrayList<>(8);
        List<RestoreSpace> restoreSpaceList = new ArrayList<>(8);
        soVslvoyList.forEach(e -> {
            if (e.getAllocationId() != null) {
                returnIdList.add(e.getId());
                restoreSpaceList.add(new RestoreSpace(e.getSoId(), Arrays.asList(e.getAllocationId())));
            }
        });
        // 同步自动批舱 重置分配
        if (CollectionUtil.isNotEmpty(returnIdList)) {
            soVslvoyService.setAllocationIdNull(returnIdList);
            autoSpaceService.restoreAllocatedSpace(restoreSpaceList);
        }
        soVslvoyList.forEach(e -> {
            resetVslvoyInfo(e, updateReqVO, userId);
        });
        //船名航次更新
        soList.forEach(e -> {
            e.setBookingVessel(updateReqVO.getVesselCode());
            e.setBookingVoyage(updateReqVO.getVoyageEx());
            e.setUpdater(userId);
            e.setUpdateTime(LocalDateTime.now());
        });
        soVslvoyService.updateBatchDO(soVslvoyList);
        //批量跟新更新状态
        soService.updateBatch(soList);
        //更改的信息
        List<SoRespVO> changeInfo = getChangeBkgVesselInfo(soList, soVslvoyList);
        //同步维护特种货船名航次
        Map<String, SoVslvoyRespVO> vslvoyListMap = changeInfo.stream().filter(vo -> null != vo.getSoVslvoyFirstLiner()).collect(Collectors.toMap(SoRespVO::getBookingNo, SoRespVO::getSoVslvoyFirstLiner));
        specialCargoApplyService.updateVslvoy(vslvoyListMap);
    }

    @Override
    public void checkVoyageBookingTime(String vesselCode, String voyage, String polCode, String timeType) {
        //在原航次截关之前（Customs Cut Off 之前
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
        VoyageBookingTimeDO voyageBookingTime = voyageBookingTimeService.getVoyageBookingTimeByVesselCodeAndVoyage(vesselCode, voyage, polCode, "EX", timeType, LocalDateTime.now().format(formatter));
        if (voyageBookingTime != null) {
            throw ServiceExceptionUtil.exception(new ErrorCode(SoErrorCodeConstants.SO_CHANGE_BKG_VESSEL_SI_CUT_OFF_ERROR.getCode(), SoErrorCodeConstants.SO_CHANGE_BKG_VESSEL_SI_CUT_OFF_ERROR.getMsg() + voyageBookingTime.getTimeValue()));
        }
    }

    public List<SoRespVO> getChangeBkgVesselInfo(List<SoDO> soList, List<SoVslvoyDO> soVslvoyList) {
        if (CollectionUtil.isEmpty(soList) || CollectionUtil.isEmpty(soVslvoyList)) {
            return new ArrayList<>();
        }
        List<SoRespVO> soRespList = SoConvert.INSTANCE.convertList(soList);
        List<SoVslvoyRespVO> soVslvoyRespList = SoVslvoyConvert.INSTANCE.convertList(soVslvoyList);
        Map<Long, SoVslvoyRespVO> soVslvoyRespMap = soVslvoyRespList.stream().collect(Collectors.toMap(SoVslvoyRespVO::getSoId, e -> e));
        soRespList.forEach(e -> {
            e.setSoVslvoyFirstLiner(soVslvoyRespMap.get(e.getId()));
        });
        return soRespList;
    }

    public void resetVslvoyInfo(SoVslvoyDO e, SoVslvoyUpdateReqVO updateReqVO, String userId) {
        e.setLaneCodeEx(updateReqVO.getLaneCodeEx());
        e.setLaneCodeIm(updateReqVO.getLaneCodeIm());
        e.setVesselCode(updateReqVO.getVesselCode());
        e.setVoyageEx(updateReqVO.getVoyageEx());
        e.setVoyageIm(updateReqVO.getVoyageIm());
        e.setTransmode(updateReqVO.getTransmode());
        e.setUpdater(userId);
        e.setUpdateTime(LocalDateTime.now());
        e.setVesselOperator(StringUtils.isNotEmpty(e.getVesselOperator()) ? e.getVesselOperator() : "");
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @Lock4j(name = "soUnique", keys = {"'awaitingMaterialUploaded'", "#userId"})
    public void awaitingMaterialUploaded(Collection<Long> ids, String reasonType, String descCn, String userId) {
        List<SoDO> solist = checkBookingProcessingAndContentPending(ids);
        List<OperateLogReqVO> operateList = new ArrayList<>();
        solist.forEach(e -> {
            e.setSoStatus(SoStatusEnum.AwaitingMaterialUploaded.getValue());
            e.setSoContentStatus(SoContentStatusEnum.PENDING_MATERIAL_UPLOADED.getValue());
            operateList.add(new OperateLogReqVO(e.getId(), e.getBlNo()));
        });
        List<OperateLogDO> operateLogList = operateLogService.batchInsert(operateList, SoSiEnum.SO.getValue(), OperateTypeEnum.SO_CONTENT_APPROVE.getValue(), SoContentStatusEnum.PENDING_MATERIAL_UPLOADED.getValue(), reasonType, descCn);
        saveSoAndOperateLogList(solist, operateLogList, userId);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @Lock4j(name = "soUnique", keys = {"'changeSoContentStatus'", "#userId"})
    public void changeSoContentStatus(Collection<Long> ids, String reasonType, String descCn, String soContentStatus, String userId) {
        List<SoDO> solist = checkBookingProcessingAndContentPending(ids);
        List<SoDetailRespVO> bcrNoPassSoList = new ArrayList<>();
        List<OperateLogReqVO> operateList = new ArrayList<>();
        solist.forEach(e -> {
            e.setSoContentStatus(soContentStatus);
            operateList.add(new OperateLogReqVO(e.getId(), e.getBlNo()));
            //审批通过校验BCR
            if (SoContentStatusEnum.APPROVED.getValue().equals(soContentStatus)) {
                SoDetailRespVO soDetail = checkSoBcr(e.getId());
                if (!soDetail.getCheckBcr()) {
                    bcrNoPassSoList.add(soDetail);
                }
            }
        });
        //批量操作如果其中有提单校验不通过，则操作取消并且提示校验不通过的提单和校验内容
        if (CollectionUtil.isNotEmpty(bcrNoPassSoList)) {
            String bcrErrorInfo = SoErrorCodeConstants.SO_BCR_ERROR.getMsg() + " " + getBcrTipInfo(bcrNoPassSoList);
            throw ServiceExceptionUtil.exception(new ErrorCode(SoErrorCodeConstants.SO_BCR_ERROR.getCode(), bcrErrorInfo));
        }
        List<OperateLogDO> operateLogList = operateLogService.batchInsert(operateList, SoSiEnum.SO.getValue(), OperateTypeEnum.SO_CONTENT_APPROVE.getValue(), soContentStatus, reasonType, descCn);
        saveSoAndOperateLogList(solist, operateLogList, userId);
        solist.forEach(e -> {
            soCommonService.syncSoStatus(e.getId(), soContentStatus);
        });
    }

    public void saveSoAndOperateLogList(List<SoDO> solist, List<OperateLogDO> operateLogList, String userId) {
        //操作日志
        Map<Long, Long> operateLogMap = operateLogList.stream().collect(Collectors.toMap(OperateLogDO::getBusinessId, OperateLogDO::getId));
        solist.forEach(e -> {
            e.setUpdater(userId);
            e.setUpdateTime(LocalDateTime.now());
            e.setOperateLogId(operateLogMap.get(e.getId()));
        });
        //批量跟新更新状态
        soService.updateBatch(solist);
    }

    public List<SoDO> checkBookingProcessingAndContentPending(Collection<Long> ids) {
        if (CollectionUtil.isEmpty(ids)) {
            throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_NOT_EXISTS);
        }
        String errorMsg = "审核内容时状态只能为订舱处理中或客户已补充材料";
        List<String> soStatusCancel = Arrays.asList(SoStatusEnum.BookingProcessing.getValue(), SoStatusEnum.CustomerUploadedMaterial.getValue());
        List<SoDO> solist = soService.checkSoStatusByUserId(ids, soStatusCancel, null, errorMsg);
        // 走BCR校验规则-》校验后落库
        long count = solist.stream().filter(e -> !Objects.equals(SoContentStatusEnum.PENDING.getValue(), e.getSoContentStatus())).count();
        if (count > 0) {
            throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_CONTENT_STATUS_ERROR);
        }
        return solist;
    }

    public String getBcrTipInfo(List<SoDetailRespVO> bcrNoPassSoList) {
        if (CollectionUtil.isNotEmpty(bcrNoPassSoList)) {
            StringBuilder errorInfo = new StringBuilder();
            bcrNoPassSoList.forEach(e -> {
                List<SoBcrCheckRespVO> bcrCheckList = e.getBcrCheckList().stream().filter(t -> t.getCheckStatus() == false).collect(Collectors.toList());
                bcrCheckList.forEach(b -> {
                    errorInfo.append("[").append(e.getSo().getBookingNo()).append("-").append(b.getRuleName()).append(StringUtils.isEmpty(b.getCodeDesc()) ? "" : "-" + b.getCodeDesc()).append("]");
                });
            });
            return errorInfo.toString();
        }
        return null;
    }

    @Override
    public void bookingComfirmPrint(HttpServletResponse response, Long soId) {
        SoDetailRespVO soDetail = getDetail(soId, StringUtils.EMPTY, false);
        if (!Objects.equals(SoStatusEnum.BookingConfirmed.getValue(), soDetail.getSo().getSoStatus())) {
            throw ServiceExceptionUtil.exception(new ErrorCode(SoErrorCodeConstants.SO_BOOKING_COMFIRM_PRINT_ERROR.getCode(), soDetail.getSo().getBookingNo() + SoErrorCodeConstants.SO_BOOKING_COMFIRM_PRINT_ERROR.getMsg()));
        }
        List<String> portCodeList = new ArrayList<>();
        Map<String, String> codeMap = new HashMap<>(16);
        soDetail.getSoVslvoyList().forEach(e -> {
            portCodeList.add(e.getPolCode());
            portCodeList.add(e.getPodCode());
            // 默认值
            codeMap.put(e.getPolCode(), e.getPolCode());
            codeMap.put(e.getPodCode(), e.getPodCode());
        });
        List<SoVslvoyRespVO> linerList = soDetail.getSoVslvoyList().stream().filter(e -> TransmodeEnum.LINER.getValue().equals(e.getTransmode())).collect(Collectors.toList());
        if (CollectionUtil.isNotEmpty(linerList) && linerList.size() > 1) {
            soDetail.getSo().setSecondTsPort(linerList.get(1).getPolCode());
        }
        PortListByCodeReqDTO var1 = new PortListByCodeReqDTO();
        var1.setPortCodeList(portCodeList);
        CommonResult<List<PortListByCodeRespDTO>> portList = portApi.listByCodeList(var1);
        //TOKYO (JPTYO) 英文名（港口代码） 在航线基础数据中查询
        if (CollectionUtil.isNotEmpty(portList.getData())) {
            portList.getData().forEach(e -> {
                codeMap.put(e.getPortCode(), e.getPortNameEn() + " (" + e.getPortCode() + ")");
            });
        }
        Map<String, String> timeMap = voyageBookingTimeService.getVoyageByVesselCodeAndVoyage(soDetail.getSo().getBookingVessel(), soDetail.getSo().getBookingVoyage(), soDetail.getSo().getPolCode(), "EX");
        //porCode 查找模板信息
        soDetail.setTimeMap(timeMap);
        soDetail.setConfirmPrintSetup(docBkConfirmPrintSetupService.getPrintByPortCode(soDetail.getSo().getPorCode()));
        new PdfUtil().buildPdfStatic(response, soDetail, codeMap);
    }

    @Override
    public FileUploadRespVo bookingComfirmPrintAddress(Long soId) {
        SoDetailRespVO soDetail = getDetail(soId, StringUtils.EMPTY, false);
        if (!Objects.equals(SoStatusEnum.BookingConfirmed.getValue(), soDetail.getSo().getSoStatus())) {
            throw ServiceExceptionUtil.exception(new ErrorCode(SoErrorCodeConstants.SO_BOOKING_COMFIRM_PRINT_ERROR.getCode(), soDetail.getSo().getBookingNo() + SoErrorCodeConstants.SO_BOOKING_COMFIRM_PRINT_ERROR.getMsg()));
        }
        List<String> portCodeList = new ArrayList<>();
        Map<String, String> codeMap = new HashMap<>(16);
        soDetail.getSoVslvoyList().forEach(e -> {
            portCodeList.add(e.getPolCode());
            portCodeList.add(e.getPodCode());
            // 默认值
            codeMap.put(e.getPolCode(), e.getPolCode());
            codeMap.put(e.getPodCode(), e.getPodCode());
        });
        List<SoVslvoyRespVO> linerList = soDetail.getSoVslvoyList().stream().filter(e -> TransmodeEnum.LINER.getValue().equals(e.getTransmode())).collect(Collectors.toList());
        if (CollectionUtil.isNotEmpty(linerList) && linerList.size() > 1) {
            soDetail.getSo().setSecondTsPort(linerList.get(1).getPolCode());
        }
        PortListByCodeReqDTO var1 = new PortListByCodeReqDTO();
        var1.setPortCodeList(portCodeList);
        CommonResult<List<PortListByCodeRespDTO>> portList = portApi.listByCodeList(var1);
        //TOKYO (JPTYO) 英文名（港口代码） 在航线基础数据中查询
        if (CollectionUtil.isNotEmpty(portList.getData())) {
            portList.getData().forEach(e -> {
                codeMap.put(e.getPortCode(), e.getPortNameEn() + " (" + e.getPortCode() + ")");
            });
        }
        Map<String, String> timeMap = voyageBookingTimeService.getVoyageByVesselCodeAndVoyage(soDetail.getSo().getBookingVessel(), soDetail.getSo().getBookingVoyage(), soDetail.getSo().getPolCode(), "EX");
        //porCode 查找模板信息
        soDetail.setTimeMap(timeMap);
        soDetail.setConfirmPrintSetup(docBkConfirmPrintSetupService.getPrintByPortCode(soDetail.getSo().getPorCode()));
        String fileName = soDetail.getSo().getBookingNo() + "BC" + LocalDateTime.now().format(FORMATTER_AMEND)+".pdf";
        codeMap.put("fileName",fileName);
        ByteArrayOutputStream byteArrayOutputStream = new PdfUtil().buildPdfStaticAddress(soDetail, codeMap);
        if (byteArrayOutputStream != null) {
            try {
                byte[] fileBytes = byteArrayOutputStream.toByteArray();
                FileUploadReqDTO fileUploadReqDTO = getFileUploadReqDTO(fileBytes, fileName,"订舱确认");
                CommonResult<FileUploadRespDTO> fileDTO = fileApi.upload(fileUploadReqDTO);
                if (fileDTO != null && fileDTO.getData() != null) {
                    return SoVslvoyConvert.INSTANCE.fileUploadRespToFileUploadRespVo(fileDTO.getData());
                }
            } catch (IOException e) {
                throw ServiceExceptionUtil.exception(BlnoBlPrintLogErrorCodeConstants.FILE_UPLOAD_FAILED);
            }
        }
        return null;
    }


    @Override
    public BookingConfirmationDataDto getBookingConfirmData(Long soId) {
        SoDetailRespVO soDetail = getDetail(soId, StringUtils.EMPTY, false);
        if (!Objects.equals(SoStatusEnum.BookingConfirmed.getValue(), soDetail.getSo().getSoStatus())) {
            throw ServiceExceptionUtil.exception(new ErrorCode(SoErrorCodeConstants.SO_BOOKING_COMFIRM_PRINT_ERROR.getCode(), soDetail.getSo().getBookingNo() + SoErrorCodeConstants.SO_BOOKING_COMFIRM_PRINT_ERROR.getMsg()));
        }
        List<String> portCodeList = new ArrayList<>();
        Map<String, String> codeMap = new HashMap<>(16);
        soDetail.getSoVslvoyList().forEach(e -> {
            portCodeList.add(e.getPolCode());
            portCodeList.add(e.getPodCode());
            // 默认值
            codeMap.put(e.getPolCode(), e.getPolCode());
            codeMap.put(e.getPodCode(), e.getPodCode());
        });
        List<SoVslvoyRespVO> linerList = soDetail.getSoVslvoyList().stream().filter(e -> TransmodeEnum.LINER.getValue().equals(e.getTransmode())).collect(Collectors.toList());
        if (CollectionUtil.isNotEmpty(linerList) && linerList.size() > 1) {
            soDetail.getSo().setSecondTsPort(linerList.get(1).getPolCode());
        }
        PortListByCodeReqDTO var1 = new PortListByCodeReqDTO();
        var1.setPortCodeList(portCodeList);
        CommonResult<List<PortListByCodeRespDTO>> portList = portApi.listByCodeList(var1);
        //TOKYO (JPTYO) 英文名（港口代码） 在航线基础数据中查询
        if (CollectionUtil.isNotEmpty(portList.getData())) {
            portList.getData().forEach(e -> {
                codeMap.put(e.getPortCode(), e.getPortNameEn() + " (" + e.getPortCode() + ")");
            });
        }
        Map<String, String> timeMap = voyageBookingTimeService.getVoyageByVesselCodeAndVoyage(soDetail.getSo().getBookingVessel(), soDetail.getSo().getBookingVoyage(), soDetail.getSo().getPolCode(), "EX");
        //porCode 查找模板信息
        soDetail.setTimeMap(timeMap);
        soDetail.setConfirmPrintSetup(docBkConfirmPrintSetupService.getPrintByPortCode(soDetail.getSo().getPorCode()));
        return this.getBookingConfirmationDataDto(soDetail,codeMap);
    }

    private @NotNull BookingConfirmationDataDto getBookingConfirmationDataDto(SoDetailRespVO soDetail, Map<String, String> codeMap){
        SoRespVO so = soDetail.getSo();
        BookingConfirmationDataDto bookingConfirmationDataDto = new BookingConfirmationDataDto();
        bookingConfirmationDataDto.setBookingNo(so.getBookingNo());
        bookingConfirmationDataDto.setDateOfIssue(LocalDate.now().toString());
        bookingConfirmationDataDto.setCarrierCode(so.getSpaceOwner());
        bookingConfirmationDataDto.setRefNo(so.getReferenceNo());
        bookingConfirmationDataDto.setBlNo(so.getBlNo());
        bookingConfirmationDataDto.setContractNo(so.getAgreementNo());
        bookingConfirmationDataDto.setBookingStatus("Booking Confirmed");
        bookingConfirmationDataDto.setServiceMode(so.getShippingTerm());
        bookingConfirmationDataDto.setBookingParty(so.getBookingParty());
        bookingConfirmationDataDto.setShipper(soDetail.getSoScn() != null ? soDetail.getSoScn().getCompanyName() : "");
        bookingConfirmationDataDto.setForwarder(so.getTopAgent());
        bookingConfirmationDataDto.setPortOfReceipt(codeMap.get(so.getPorCode()));
        bookingConfirmationDataDto.setPortOfLoading(codeMap.get(so.getPolCode()));
        bookingConfirmationDataDto.setVesselVoy(so.getBookingVessel() + " " + so.getBookingVoyage());
        bookingConfirmationDataDto.setPortOfDelivery(codeMap.get(so.getDelCode()));
        bookingConfirmationDataDto.setEtd(so.getPolEtd());
        bookingConfirmationDataDto.setEta(so.getPodEta());
        SoCargoRespVO cargo = soDetail.getSoCargoList().get(0);
        StringBuilder containerQtyTypes = new StringBuilder();
        soDetail.getSoForecastList().forEach(e -> {
            containerQtyTypes.append(e.getCntrQuantity()).append(" * ").append(e.getCntrSize()).append(e.getCntrType()).append(" G.W. ").append(e.getPerCntrWgt().toPlainString()).append("KGS ");
        });
        bookingConfirmationDataDto.setContainerQtyTypes(containerQtyTypes.toString());
        bookingConfirmationDataDto.setCommodityName(cargo.getCommodityCn() + " " + cargo.getCommodityEn() + " " + cargo.getHsCode());
        // 特种货查询
        bookingConfirmationDataDto.setTemperature(cargo.getTemperature()+cargo.getTemperatureUnit());
        bookingConfirmationDataDto.setVentilation(cargo.getVantilation());
        bookingConfirmationDataDto.setUnNo(cargo.getUnNo());
        bookingConfirmationDataDto.setImoClass(cargo.getImoClass());
//        bookingConfirmationDataDto.setPickupLocation("提箱堆场地址，联系方式，堆场代码等");
//        bookingConfirmationDataDto.setReturnCyLocation("还箱堆场，联系方式，码头代码等等");
//        bookingConfirmationDataDto.setTitle("SINOLINES BOOKING CONFIRMATION");
        // porCode 查找模板，可以为空
        DocBkConfirmPrintSetupRespVO confirmPrintSetup = soDetail.getConfirmPrintSetup();
        if (confirmPrintSetup != null) {
            bookingConfirmationDataDto.setCompanyName(confirmPrintSetup.getCompanyName());
            bookingConfirmationDataDto.setContractNo(confirmPrintSetup.getTelephone() + " " + confirmPrintSetup.getEmail());
            bookingConfirmationDataDto.setPortSpecificRemarks(confirmPrintSetup.getRemark());
        }
        return bookingConfirmationDataDto;
    }




    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    @Lock4j(name = "soUnique", keys = {"'checkSoBcr'", "#soId"})
    public SoDetailRespVO checkSoBcr(Long soId) {
        SoDetailRespVO soDetail = getDetail(soId, StringUtils.EMPTY, false);
        checkSoBcr(soDetail);
        return soDetail;
    }

    public void checkSoBcr(SoDetailRespVO soDetail) {
        SoRespVO so = soDetail.getSo();
        setSoVslvoyFirstLinerAndEtd(soDetail.getSoVslvoyList(), so);
        //先删除历史BCR校验 物理删除
        soBcrCheckService.deleteBySoId(so.getId());
        List<LimitRuleRespVO> limitList = findRuleBySo(so);
        checkEtdTime(limitList, so);
        if (CollectionUtil.isEmpty(limitList)) {
            return;
        }
        //查找规则详情
        limitRuleService.matchAttatchAndConfirmAndRuleItem(limitList);
        //转换 拼接SO的信息 字段信息
        Map<String, List<String>> fieldMap = FilterConditionFieldEnum.getAllField();
        List<Map<String, Object>> cargoMapList = new ArrayList<>();
        List<Map<String, Object>> vslvoyMapList = new ArrayList<>();
        List<Map<String, Object>> forecastMapList = new ArrayList<>();
        List<Map<String, Object>> requireMapList = new ArrayList<>();
        List<LimitRuleRespVO> bcrRuleList = new ArrayList<>();
        Map<String, Object> soMap = invokeGetValue(soDetail.getSo(), fieldMap.get("SO"));
        soDetail.getSoCargoList().forEach(e -> {
            cargoMapList.add(invokeGetValue(e, fieldMap.get("Cargo")));
        });
        soDetail.getSoVslvoyList().forEach(e -> {
            vslvoyMapList.add(invokeGetValue(e, fieldMap.get("Vslvoy")));
        });
        soDetail.getSoForecastList().forEach(e -> {
            forecastMapList.add(invokeGetValue(e, fieldMap.get("Forecast")));
        });
        soDetail.getRequirementsList().forEach(e -> {
            requireMapList.add(invokeGetValue(e, fieldMap.get("Require")));
        });
        limitList.forEach(e -> {
            if (checkRule(e, soMap, cargoMapList, vslvoyMapList, forecastMapList, requireMapList)) {
                // 规则匹配上
                bcrRuleList.add(e);
            }
        });
        //统计校验规则 结果保存BCR校验规则
        List<SoBcrCheckCreateReqVO> createList = new ArrayList<>();
        Map<String, AttachmentRespVO> soAttachmentMap = soDetail.getAttachmentList().stream().collect(Collectors.toMap(AttachmentRespVO::getAttachTypeCode, e -> e));
        Map<String, RequirementsRespVO> soRequirementMap = soDetail.getRequirementsList().stream().collect(Collectors.toMap(RequirementsRespVO::getRequireCode, e -> e));
        bcrRuleList.forEach(e -> {
            // 检验规则要求与现有的附件或要求是否匹配
            checkBcr(e, createList, so, soAttachmentMap, soRequirementMap);
        });
        if (CollectionUtil.isNotEmpty(createList)) {
            List<SoBcrCheckRespVO> bcrCheckList = soBcrCheckService.insertBatch(createList);
            //添加BCR校验数据
            soDetail.setBcrCheckList(bcrCheckList);
        }
    }

    private static final String RULE_TYPE_PROHIBITED = "PROHIBITED";
    private static final String RULE_TYPE_SUPPLEMENTARY_MATERIALS = "SUPPLEMENTARY_MATERIALS";
    private static final String RULE_TYPE_INFORMATION_CONFIRM = "INFORMATION_CONFIRM";

    public void checkBcr(LimitRuleRespVO e, List<SoBcrCheckCreateReqVO> createList, SoRespVO so,
                         Map<String, AttachmentRespVO> soAttachmentMap, Map<String, RequirementsRespVO> soRequirementMap) {
        // 禁接 匹配上就默认不通过
        if (RULE_TYPE_PROHIBITED.equals(e.getRuleType())) {
            createList.add(new SoBcrCheckCreateReqVO(so.getId(), e.getId(), e.getRuleType(), e.getRuleName(), e.getFeedbackContent(), false));
        }
        // 补充材料->提单附件表
        if (RULE_TYPE_SUPPLEMENTARY_MATERIALS.equals(e.getRuleType())) {
            if (CollectionUtil.isNotEmpty(e.getAttatchItemList())) {
                e.getAttatchItemList().forEach(a -> {
                    SoBcrCheckCreateReqVO checkAttatch = new SoBcrCheckCreateReqVO(so.getId(), e.getId(), e.getRuleType(), e.getRuleName(), a.getReqMatCode(), a.getReqMatNameCn(), false);
                    if (soAttachmentMap.get(a.getReqMatCode()) != null) {
                        checkAttatch.setCheckStatus(true);
                    }
                    createList.add(checkAttatch);
                });
            } else {
                createList.add(new SoBcrCheckCreateReqVO(so.getId(), e.getId(), e.getRuleType(), e.getRuleName(), e.getFeedbackContent(), false));
            }
        }
        // 确认项->订舱附加要求表
        if (RULE_TYPE_INFORMATION_CONFIRM.equals(e.getRuleType())) {
            if (CollectionUtil.isNotEmpty(e.getConfirmItemList())) {
                e.getConfirmItemList().forEach(a -> {
                    SoBcrCheckCreateReqVO checkAttatch = new SoBcrCheckCreateReqVO(so.getId(), e.getId(), e.getRuleType(), e.getRuleName(), a.getConfirmCode(), a.getConfirmNameCn(), false);
                    if (soRequirementMap.get(a.getConfirmCode()) != null) {
                        checkAttatch.setCheckStatus(true);
                    }
                    createList.add(checkAttatch);
                });
            } else {
                createList.add(new SoBcrCheckCreateReqVO(so.getId(), e.getId(), e.getRuleType(), e.getRuleName(), e.getFeedbackContent(), false));
            }
        }
    }

    public List<LimitRuleRespVO> findRuleBySo(SoRespVO so) {
        LimitRulePageReqVO reqVO = new LimitRulePageReqVO();
        return limitRuleService.findLimitRule(reqVO, "BCR");
    }

    public void checkEtdTime(List<LimitRuleRespVO> limitList, SoRespVO so) {
        ZoneId zoneId = ZoneId.systemDefault();
        for (int i = 0; i < limitList.size(); i++) {
            LimitRuleRespVO rule = limitList.get(i);
            //预计离港时间 是否符合 实际离港ATD目前没法判断
            if ("ETD".equals(rule.getEffectiveDateType()) && StringUtils.isNotEmpty(so.getPolEtd())) {
                DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
                LocalDateTime polEtdDate = LocalDateTime.parse(so.getPolEtd(), formatter);
                if (rule.getEffictiveDate() != null) {
                    LocalDate localDate = rule.getEffictiveDate().toInstant().atZone(zoneId).toLocalDate();
                    LocalDateTime effictiveDate = LocalDateTime.of(localDate, LocalTime.MIN);
                    if (effictiveDate.isAfter(polEtdDate)) {
                        limitList.remove(i);
                        i--;
                        continue;
                    }
                }
                if (rule.getExpiringDate() != null) {
                    LocalDate localDate = rule.getExpiringDate().toInstant().atZone(zoneId).toLocalDate();
                    LocalDateTime expiringDate = LocalDateTime.of(localDate, LocalTime.MAX);
                    if (expiringDate.isBefore(polEtdDate)) {
                        limitList.remove(i);
                        i--;
                    }
                }
            }
        }
    }

    public boolean checkRule(LimitRuleRespVO rule, Map<String, Object> soMap, List<Map<String, Object>> cargoMapList,
                             List<Map<String, Object>> vslvoyMapList, List<Map<String, Object>> forecastMapList,
                             List<Map<String, Object>> requireMapList) {
        if (rule.getRuleItem() == null) {
            return true;
        }
        for (int i = 0; i < cargoMapList.size(); i++) {
            Map<String, Object> cargoMapTemp = cargoMapList.get(i);
            for (int j = 0; j < vslvoyMapList.size(); j++) {
                Map<String, Object> vslvoyMapTemp = vslvoyMapList.get(j);
                for (int k = 0; k < forecastMapList.size(); k++) {
                    Map<String, Object> forecastMapTemp = forecastMapList.get(k);
                    Map<String, Object> allTemp = new HashMap<>(16);
                    allTemp.putAll(soMap);
                    allTemp.putAll(cargoMapTemp);
                    allTemp.putAll(vslvoyMapTemp);
                    allTemp.putAll(forecastMapTemp);
                    if (CollectionUtil.isNotEmpty(requireMapList)) {
                        for (int m = 0; m < requireMapList.size(); m++) {
                            allTemp.putAll(requireMapList.get(m));
                            boolean check = CmclinkExpressionUtil.check(rule.getRuleItem().getMatchExpression(), allTemp);
                            //判断通过 BCR留档
                            if (check) {
                                return true;
                            }
                        }
                    } else {
                        boolean check = CmclinkExpressionUtil.check(rule.getRuleItem().getMatchExpression(), allTemp);
                        //判断通过 BCR留档
                        if (check) {
                            return true;
                        }
                    }
                }
            }
        }
        return false;
    }

    public Map<String, Object> invokeGetValue(Object info, List<String> fields) {
        Map<String, Object> valueMap = new HashMap<>(16);
        BeanMap beanMap = new org.apache.commons.beanutils.BeanMap(info);
        for (String field : fields) {
            Object obj = beanMap.get(field);
            if (obj != null) {
                valueMap.put(field, obj);
            }
        }
        return valueMap;
    }

    @Override
    public void updateConfirmedSoAmendCheck(Long soId, String userId) {
        SoDO oldSo = checkStatusForSoAmend(soId, userId);
        List<SoVslvoyDO> soVslvoyList = soVslvoyService.seleteBySoId(soId);
        checkOtherForSoAmend(oldSo, soVslvoyList);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @Lock4j(name = "soUnique", keys = {"'updateConfirmedSo'", "#userId"})
    public Long updateConfirmedSoAmend(SoCreateAndUpdateReqVo createReqVO, String userId) {
        if (createReqVO.getOldId() == null || StringUtils.isEmpty(createReqVO.getApplyReason())) {
            throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_UPDATE_CONFIRMED_NULL_ERROR);
        }
        SoDO oldSo = checkStatusForSoAmend(createReqVO.getOldId(), userId);
        List<SoVslvoyDO> soVslvoyList = soVslvoyService.seleteBySoId(oldSo.getId());
        checkOtherForSoAmend(oldSo, soVslvoyList);
        // 不再修改船名航次，取旧的数据
        List<SoVslvoyUpdateReqVO> soVslvoyUpdateList = SoVslvoyConvert.INSTANCE.convertUpdateList(soVslvoyList);
        soVslvoyUpdateList.forEach(e -> {
            e.setId(null);
            e.setSoId(null);
        });
        createReqVO.setSoVslvoyList(soVslvoyUpdateList);
        Long newId = createOrUpdateSo(createReqVO, false, userId, false);
        SoDO newSo = soService.getSo(newId);
        //审批通过后修改审批中,绑定旧的ID，状态为0默认不显示，中间状态
        newSo.setBookingTime(oldSo.getBookingTime());
        newSo.setValidStatus(ValidStatusEnum.AMEND.getValue());
        newSo.setOldId(oldSo.getId());
        newSo.setBookingNo(oldSo.getBookingNo());
        newSo.setBlNo(oldSo.getBlNo());
        soService.updateById(newSo);
        //添加申请日志
        SoAmendApplyCreateReqVO soAmendApply = new SoAmendApplyCreateReqVO();
        //订单号11位+SO+yy+mm+dd+hh4+mi+ss
        soAmendApply.setSoAmendApplyNo(newSo.getBookingNo() + SoSiEnum.SO.getValue() + LocalDateTime.now().format(FORMATTER_AMEND));
        soAmendApply.setBookingNo(newSo.getBookingNo());
        soAmendApply.setSoId(newSo.getId());
        soAmendApply.setApplyReason(createReqVO.getApplyReason());
        soAmendApply.setSoAmendStatus(ContentChangeStatusEnum.BOOKING_CHANGE_PROCESSING.getValue());
        soAmendApply.setApplyTime(LocalDateTime.now());
        soAmendApply.setApplyUser(userId);
        soAmendApply.setOperatianType(SoAmendOperateTypeEnum.AMEND_SO.getValue());
        SoAmendApplyDO apply = soAmendApplyService.createSoAmendApply(soAmendApply);
        // 新旧数据对比，内容审批、批舱、SO主状态是否发生变化
        reSetSoStatus(apply.getId());
        return newId;
    }

    public SoDO checkStatusForSoAmend(Long soId, String userId) {
        //4、当前 SO 状态=【订舱已确认】
        //5、当前 SO 不存在修改申请审批中记录状态是【订舱更改处理中】【订舱更改待补充材料】【客户取消订舱处理中】
        SoDO oldSo = soService.getSo(soId);
        if (oldSo == null) {
            throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_NOT_EXISTS);
        }
        checkUserLimit(oldSo.getCreator(), userId);
        if (!Objects.equals(SoStatusEnum.BookingConfirmed.getValue(), oldSo.getSoStatus())) {
            throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_UPDATE_CONFIRMED_ERROR);
        }
        SoAmendApplyDO applyLast = soAmendApplyService.getByBookingNo(oldSo.getBookingNo(), Arrays.asList(ContentChangeStatusEnum.BOOKING_CHANGE_PROCESSING.getValue(), ContentChangeStatusEnum.BOOKING_CHANGE_AWAITING_MATERIAL_UPLOADED.getValue(), ContentChangeStatusEnum.BOOKING_CANCEL_PROCESSING.getValue()));
        if (applyLast != null) {
            throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_UPDATE_CONFIRMED_DOU_ERROR);
        }
        return oldSo;
    }

    public void checkOtherForSoAmend(SoDO oldSo, List<SoVslvoyDO> soVslvoyList) {
        //1、当前 SO 订舱船名航次没有锁船
        //2、当前 SO 不存在预配舱单
        //3、当前 SO 不存在SI记录
        checkVslvoyAmend(soVslvoyList);
        if (CollectionUtil.isNotEmpty(manifestService.findByBookingNos(Arrays.asList(oldSo.getBookingNo())))) {
            throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_AMEND_MANIFEST_ERROR);
        }
        LambdaQueryWrapperX<BlnoDO> queryWrapperX = new LambdaQueryWrapperX<>();
        queryWrapperX.eq(BlnoDO::getBlNo, oldSo.getBlNo());
        Long count = blnoMapper.selectCount(queryWrapperX);
        if (count > 0) {
            throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_AMEND_SI_ERROR);
        }
    }

    public void checkOtherForSoCancel(SoDO oldSo, List<SoVslvoyDO> soVslvoyList) {
        //1、当前 SO 订舱船名航次没有锁船
        //4、当前 SO 的 SI 没有签发提单 SIGN TYPE=空。
        //5、当前 SO 的 SI 没有计算费用。
        checkVslvoyAmend(soVslvoyList);
        LambdaQueryWrapperX<BlnoDO> queryWrapperX = new LambdaQueryWrapperX<>();
        queryWrapperX.eq(BlnoDO::getBlNo, oldSo.getBlNo());
        queryWrapperX.eq(BlnoDO::getPodBlFlag, true);
        queryWrapperX.eq(BlnoDO::getValid, SiValidEnum.VALID.getValue());
        BlnoDO blno = blnoMapper.selectOne(queryWrapperX);
        if (blno != null) {
            if (Objects.equals(true, blno.getBlLockFlag())) {
                throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_AMEND_CANCEL_SI_LOCK_ERROR);
            }
            if (StringUtils.isNotEmpty(blno.getSignType())) {
                throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_AMEND_CANCEL_SI_ERROR);
            }
        }
    }

    public List<BlnoFreightDO> getBlnoFreightByBlnoId(Long blnoId) {
        LambdaQueryWrapper<BlnoFreightDO> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(BlnoFreightDO::getBlnoId, blnoId);
        return blnoFreightMapper.selectList(queryWrapper);
    }

    public List<BlnoLocalChargeDO> getBlnoLocalChargeByBlnoId(Long blnoId) {
        LambdaQueryWrapper<BlnoLocalChargeDO> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(BlnoLocalChargeDO::getBlnoId, blnoId);
        return blnoLocalChargeMapper.selectList(queryWrapper);
    }

    public List<BlnoLocalCostDO> getBlnoLocalCostByBlnoId(Long blnoId) {
        LambdaQueryWrapper<BlnoLocalCostDO> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(BlnoLocalCostDO::getBlnoId, blnoId);
        return blnoLocalCostMapper.selectList(queryWrapper);
    }


    @Override
    @Transactional(rollbackFor = Exception.class)
    @Lock4j(name = "soUnique", keys = {"'cancelSoAmend'", "#userId"})
    public void cancelSoAmend(SoCancelReqVo cancelReqVO, String userId) {
        SoDO oldSo = soService.getSo(cancelReqVO.getSoId());
        if (oldSo == null) {
            throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_NOT_EXISTS);
        }
        checkUserLimit(oldSo.getCreator(), userId);
        List<SoVslvoyDO> soVslvoyList = soVslvoyService.seleteBySoId(oldSo.getId());
        checkOtherForSoCancel(oldSo, soVslvoyList);
        cancelSo(oldSo);
    }

    public void cancelSoAmendApply(SoDO oldSo, SoCancelReqVo cancelReqVO, String userId) {
        //添加申请日志
        SoAmendApplyCreateReqVO soAmendApply = new SoAmendApplyCreateReqVO();
        //订单号11位+SO+yy+mm+dd+hh4+mi+ss
        soAmendApply.setSoAmendApplyNo(oldSo.getBookingNo() + SoSiEnum.SO.getValue() + LocalDateTime.now().format(FORMATTER_AMEND));
        soAmendApply.setBookingNo(oldSo.getBookingNo());
        soAmendApply.setSoId(oldSo.getId());
        soAmendApply.setApplyReason(cancelReqVO.getApplyReason());
        soAmendApply.setSoAmendStatus(ContentChangeStatusEnum.BOOKING_CANCEL_PROCESSING.getValue());
        soAmendApply.setApplyTime(LocalDateTime.now());
        soAmendApply.setApplyUser(userId);
        soAmendApply.setOperatianType(SoAmendOperateTypeEnum.CANCEL_BOOKING.getValue());
        soAmendApplyService.createSoAmendApply(soAmendApply);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @Lock4j(name = "soUnique", keys = {"'updateConfirmedSo'", "#userId"})
    public Long updateConfirmedSoAmendVessel(SoCreateAndUpdateReqVo createReqVO, String userId) {
        if (CollectionUtil.isEmpty(createReqVO.getSoVslvoyList())) {
            Assertion.assertTrue(false).raise0(SoErrorCodeConstants.SO_CHECK_NULL.getCode(), "Voyage Route");
        }
        SoDO oldSo = checkStatusForSoAmend(createReqVO.getOldId(), userId);
        List<SoVslvoyDO> soVslvoyList = soVslvoyService.seleteBySoId(oldSo.getId());
        checkOtherForSoAmend(oldSo, soVslvoyList);
        //拼接操船方
        List<String> vesselCodeList = createReqVO.getSoVslvoyList().stream().map(e -> e.getVesselCode()).collect(Collectors.toList());
        Map<String, String> vesselOperatorMap = getVesselOperatorByCodes(vesselCodeList);
        createReqVO.getSoVslvoyList().forEach(e -> {
            e.setVesselOperator(vesselOperatorMap.get(e.getVesselCode()));
        });
        // 检查数据是否修改
        checkChangeVslvoy(soVslvoyList, createReqVO.getSoVslvoyList());
        Long newId = updateConfirmedSoAmendVessel(createReqVO);
        SoDO newSo = soService.getSo(newId);
        //审批通过后修改审批中,绑定旧的ID，状态为0默认不显示，中间状态
        newSo.setValidStatus(ValidStatusEnum.AMEND.getValue());
        //内容默认通过
        newSo.setSoStatus(SoStatusEnum.BookingProcessing.getValue());
        newSo.setSoContentStatus(SoContentStatusEnum.APPROVED.getValue());
        newSo.setOldId(oldSo.getId());
        newSo.setBookingNo(oldSo.getBookingNo());
        newSo.setBlNo(oldSo.getBlNo());
        newSo.setBookingTime(oldSo.getBookingTime());
        soService.updateById(newSo);
        if (Objects.equals(true, createReqVO.getSpecialCargoFlag())) {
            specialModifyVelVoyService.specidalModifyVelVoy(new SpecialModifyVelVoyDto(oldSo.getSoStatus(), oldSo.getId(), newSo.getId(), oldSo.getBookingNo(), createReqVO.getSoVslvoyList()));
        }
        //添加申请日志
        SoAmendApplyCreateReqVO soAmendApply = new SoAmendApplyCreateReqVO();
        soAmendApply.setSoAmendApplyNo(newSo.getBookingNo() + SoSiEnum.SO.getValue() + LocalDateTime.now().format(FORMATTER_AMEND));
        soAmendApply.setBookingNo(newSo.getBookingNo());
        soAmendApply.setSoId(newSo.getId());
        soAmendApply.setApplyReason(createReqVO.getApplyReason());
        soAmendApply.setSoAmendStatus(ContentChangeStatusEnum.BOOKING_CHANGE_PROCESSING.getValue());
        soAmendApply.setApplyTime(LocalDateTime.now());
        soAmendApply.setApplyUser(userId);
        soAmendApply.setOperatianType(SoAmendOperateTypeEnum.CHANGE_VESSEL.getValue());
        soAmendApplyService.createSoAmendApply(soAmendApply);
        return newId;
    }

    public void checkChangeVslvoy(List<SoVslvoyDO> soVslvoyList, List<SoVslvoyUpdateReqVO> soVslvoyNewList) {
        boolean checkChange = false;
        //设置默认状态
        soVslvoyNewList.forEach(e -> {
            if (TransmodeEnum.LINER.getValue().equals(e.getTransmode())) {
                e.setSpaceApproveStatus(ApproveStatusEnum.PENDING.getValue());
            } else {
                e.setSpaceApproveStatus(ApproveStatusEnum.APPROVED.getValue());
            }
        });

        if (soVslvoyList.size() == soVslvoyNewList.size()) {
            List<SoVslvoyRespVO> soVslvoyOldList = SoVslvoyConvert.INSTANCE.convertList(soVslvoyList);
            for (int i = 0; i < soVslvoyOldList.size(); i++) {
                String checkVslvoy = CompareUtils.compareByFieldsString(soVslvoyOldList.get(i), soVslvoyNewList.get(i), SoVslvoyBaseVO.getMultiFieldComparator());
                if (StringUtils.isNotEmpty(checkVslvoy)) {
                    checkChange = true;
                } else {
                    // 数据没有变动,直接复制
                    SoVslvoyUpdateReqVO sameVslvoy = SoVslvoyConvert.INSTANCE.convertUpdate(soVslvoyOldList.get(i));
                    sameVslvoy.setId(null);
                    sameVslvoy.setSoId(null);
                    soVslvoyNewList.set(i, sameVslvoy);
                }
            }
        } else {
            checkChange = true;
        }
        if (checkChange == false) {
            throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_CHANGE_BKG_VESSEL_SAME_ERROR);
        }
        long count = soVslvoyNewList.stream().filter(e -> ApproveStatusEnum.PENDING.getValue().equals(e.getSpaceApproveStatus())).count();
        // 多式联运
        if (count == 0) {
            throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_CHANGE_BKG_VESSEL_NO_ERROR);
        }
    }

    @Transactional(rollbackFor = Exception.class)
    public void reSetSoStatus(Long applyId) {
        // 新旧数据对比，复制状态
        SoDetailCompareRespVo compareResp = getAmendApplyDetail(applyId);
        SoDetailRespVO newSoDetail = compareResp.getNewSo();
        SoDetailRespVO oldSoDetail = compareResp.getOldSo();
        SoRespVO newSo = newSoDetail.getSo();
        SoRespVO oldSo = oldSoDetail.getSo();
        newSo.setCompareResult(CompareUtils.compareByFields(newSo, oldSo, SoRespVO.getMultiFieldComparator()));
        newSo.setCompareFirstVslvoyResult(CompareUtils.compareByFields(newSo, oldSo, SoRespVO.getMultiFieldFirstVslvoyComparator()));
        // 航次前后没有依赖关系，都是单独判断
        for (int i = 0; i < newSoDetail.getSoForecastList().size(); i++) {
            for (int j = 0; j < oldSoDetail.getSoForecastList().size(); j++) {
                SoForecastRespVO newTemp = newSoDetail.getSoForecastList().get(i);
                SoForecastRespVO oldTemp = oldSoDetail.getSoForecastList().get(j);
                boolean compareResult = CompareUtils.compareByFields(newTemp, oldTemp, SoForecastRespVO.getMultiFieldComparator());
                if (compareResult) {
                    newTemp.setCompareVslvoyResult(true);
                    oldTemp.setCompareVslvoyResult(true);
                    if (newTemp.getPerCntrWgt().compareTo(oldTemp.getPerCntrWgt()) != 0) {
                        //前提条件
                        newTemp.setCompareResult(false);
                        oldTemp.setCompareResult(false);
                    }
                }
            }
        }

        for (int i = 0; i < newSoDetail.getSoCargoList().size(); i++) {
            for (int j = 0; j < oldSoDetail.getSoCargoList().size(); j++) {
                SoCargoRespVO newTemp = newSoDetail.getSoCargoList().get(i);
                SoCargoRespVO oldTemp = oldSoDetail.getSoCargoList().get(j);
                boolean compareResult = CompareUtils.compareByFields(newTemp, oldTemp, SoCargoRespVO.getMultiFieldComparator());
                if (compareResult) {
                    newTemp.setCompareResult(true);
                    oldTemp.setCompareResult(true);
                }
            }
        }

        for (int i = 0; i < newSoDetail.getAttachmentList().size(); i++) {
            for (int j = 0; j < oldSoDetail.getAttachmentList().size(); j++) {
                AttachmentRespVO newTemp = newSoDetail.getAttachmentList().get(i);
                AttachmentRespVO oldTemp = oldSoDetail.getAttachmentList().get(j);
                boolean compareResult = CompareUtils.compareByFields(newTemp, oldTemp, AttachmentRespVO.getMultiFieldComparator());
                if (compareResult) {
                    newTemp.setCompareResult(true);
                    oldTemp.setCompareResult(true);
                }
            }
        }

        for (int i = 0; i < newSoDetail.getRequirementsList().size(); i++) {
            for (int j = 0; j < oldSoDetail.getRequirementsList().size(); j++) {
                RequirementsRespVO newTemp = newSoDetail.getRequirementsList().get(i);
                RequirementsRespVO oldTemp = oldSoDetail.getRequirementsList().get(j);
                boolean compareResult = CompareUtils.compareByFields(newTemp, oldTemp, RequirementsRespVO.getMultiFieldComparator());
                if (compareResult) {
                    newTemp.setCompareResult(true);
                    oldTemp.setCompareResult(true);
                }
            }
        }
        boolean compareSo = newSoDetail.getSo().getCompareResult();
        long compareForecast = newSoDetail.getSoForecastList().stream().filter(e -> e.getCompareResult() == false).count();
        long compareCargo = newSoDetail.getSoCargoList().stream().filter(e -> e.getCompareResult() == false).count();
        long compareAttachment = newSoDetail.getAttachmentList().stream().filter(e -> e.getCompareResult() == false).count();
        long compareRequirement = newSoDetail.getRequirementsList().stream().filter(e -> e.getCompareResult() == false).count();

        long compareForecastOld = oldSoDetail.getSoForecastList().stream().filter(e -> e.getCompareResult() == false).count();
        long compareCargoOld = oldSoDetail.getSoCargoList().stream().filter(e -> e.getCompareResult() == false).count();
        long compareAttachmentOld = oldSoDetail.getAttachmentList().stream().filter(e -> e.getCompareResult() == false).count();
        long compareRequirementOld = oldSoDetail.getRequirementsList().stream().filter(e -> e.getCompareResult() == false).count();

        boolean compareFirstVslvoyResult = newSoDetail.getSo().getCompareFirstVslvoyResult();
        long compareVslvoyResult = newSoDetail.getSoForecastList().stream().filter(e -> e.getCompareVslvoyResult() == false).count();
        long compareVslvoyResultOld = oldSoDetail.getSoForecastList().stream().filter(e -> e.getCompareVslvoyResult() == false).count();

        newSo.setSoContentStatus(SoContentStatusEnum.APPROVED.getValue());
        newSo.setSoStatus(SoStatusEnum.BookingConfirmed.getValue());
        boolean checkContentFalse = compareSo == false || compareForecast > 0 || compareCargo > 0 || compareAttachment > 0 || compareRequirement > 0
                || compareForecastOld > 0 || compareCargoOld > 0 || compareAttachmentOld > 0 || compareRequirementOld > 0;
        if (checkContentFalse) {
            newSo.setSoContentStatus(SoContentStatusEnum.PENDING.getValue());
        }
        boolean checkforecastFalse = compareVslvoyResult > 0 || compareVslvoyResultOld > 0;
        // 特种货 SO
        if (checkforecastFalse || compareForecast > 0 || compareForecastOld > 0) {
            if (SoCargoRespVO.checkSpecialCargo(oldSoDetail.getSoCargoList().get(0))) {
                throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_AMEND_SPECIAL_ERROR);
            }
        }
        //预配有变动 重新批舱
        int line = 0;
        boolean checkVslvoy = true;
        for (SoVslvoyRespVO e : newSoDetail.getSoVslvoyList()) {
            if (TransmodeEnum.LINER.getValue().equals(e.getTransmode())) {
                if (checkforecastFalse) {
                    e.setSpaceApproveStatus(ApproveStatusEnum.PENDING.getValue());
                    e.setReturnAllocationId(e.getAllocationId());
                    checkVslvoy = false;
                    continue;
                }
                line++;
                if (line == 1 && compareFirstVslvoyResult == false) {
                    e.setSpaceApproveStatus(ApproveStatusEnum.PENDING.getValue());
                    e.setReturnAllocationId(e.getAllocationId());
                    checkVslvoy = false;
                    continue;
                }
            }
            e.setSpaceApproveStatus(ApproveStatusEnum.APPROVED.getValue());
        }
        if (checkVslvoy == false || SoContentStatusEnum.PENDING.getValue().equals(newSo.getSoContentStatus())) {
            newSo.setSoStatus(SoStatusEnum.BookingProcessing.getValue());
        }
        if (SoStatusEnum.BookingConfirmed.getValue().equals(newSo.getSoStatus())) {
            throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_AMEND_NO_UPDATE_ERROR);
        }
        // 更新数据状态 同步
        if (CollectionUtil.isNotEmpty(newSoDetail.getSoVslvoyList())) {
            soVslvoyService.updateBatchDO(SoVslvoyConvert.INSTANCE.convertBatchRespUpdate(newSoDetail.getSoVslvoyList()));
        }
        soService.updateStatusAndContentStatus(newSo.getId(), newSo.getSoStatus(), newSo.getSoContentStatus());
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @Lock4j(name = "soUnique", keys = {"'cancelSoAudit'", "#amendApply.id"})
    public void cancelSoAudit(SoAmendApplyUpdateReqVO amendApply, String userId) {
        SoAmendApplyDO amendApplyOld = soAmendApplyService.getSoAmendApply(amendApply.getId());
        if (amendApplyOld == null) {
            throw ServiceExceptionUtil.exception(SoAmendApplyErrorCodeConstants.SO_AMEND_APPLY_NOT_EXISTS);
        }
        if (!Objects.equals(amendApplyOld.getSoAmendStatus(), ContentChangeStatusEnum.BOOKING_CANCEL_PROCESSING.getValue())) {
            throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_BOOKING_DRAFT_CHANGE);
        }
        List<String> status = Arrays.asList(ContentChangeStatusEnum.BOOKING_CANCEL_CONFIRMED.getValue(), ContentChangeStatusEnum.BOOKING_CANCEL_REJECTED.getValue());
        if (!status.contains(amendApply.getSoAmendStatus())) {
            throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_UPDATE_CONFIRMED_APPLY_ERROR);
        }
        SoDO so = soService.getSo(amendApplyOld.getSoId());
        if (so == null) {
            throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_NOT_EXISTS);
        }
        if (Objects.equals(ContentChangeStatusEnum.BOOKING_CANCEL_CONFIRMED.getValue(), amendApply.getSoAmendStatus())) {
            cancelSo(so);
        }
        List<OperateLogReqVO> operateList = new ArrayList<>();
        operateList.add(new OperateLogReqVO(so.getId(), so.getBlNo()));
        List<OperateLogDO> operateLogList = operateLogService.batchInsert(operateList, SoSiEnum.SO.getValue(), OperateTypeEnum.SO_CONTENT_APPROVE.getValue(), amendApply.getSoAmendStatus(), amendApply.getReasonType(), amendApply.getDescCn());
        saveSoAndOperateLogList(Arrays.asList(soService.getSo(so.getId())), operateLogList, userId);
        //操作保存
        amendApplyOld.setSoAmendStatus(amendApply.getSoAmendStatus());
        amendApplyOld.setApplyUser(userId);
        amendApplyOld.setApplyTime(LocalDateTime.now());
        soAmendApplyService.updateById(amendApplyOld);
    }

    @Transactional(rollbackFor = Exception.class)
    public void cancelSo(SoDO so) {
        //如果是特种货需要把特种货申请也更新为取消状态。
        soCommonService.syncSoStatus(so.getId(), SoStatusEnum.BookingCancelledByCustomer.getValue());
        //如果存在 SI 需要把 SI 记录逻辑删除。
        LambdaUpdateWrapper<BlnoDO> queryWrapperX = new LambdaUpdateWrapper<>();
        queryWrapperX.eq(BlnoDO::getBlNo, so.getBlNo());
        queryWrapperX.eq(BlnoDO::getDeleted, false);
        List<BlnoDO> blList = blnoMapper.selectList(queryWrapperX);
        if (CollectionUtil.isNotEmpty(blList)) {
            //如果存在 SI 修改申请中记录则删除申请记录。
            //如果存在拆并单申请审批记录则删除拆并单申请审批记录。
            List<Long> blIds = blList.stream().map(e -> e.getId()).collect(Collectors.toList());
            LambdaUpdateWrapper<ConfirmNoticeDO> confirmNoticeWrapper = new LambdaUpdateWrapper<>();
            confirmNoticeWrapper.in(ConfirmNoticeDO::getBlnoId, blIds);
            confirmNoticeMapper.delete(confirmNoticeWrapper);

            List<BlnoSpiltCombineDO> blnoSpiltCombineList = blnoSpiltCombineMapper.selectSpiltCombineByOldBlnoIds(blIds, SiSplitCombineApproveStatusEnum.PENDING.getValue());
            for (BlnoSpiltCombineDO sc : blnoSpiltCombineList) {
                blnoSpiltCombineService.deleteBlnoSpiltCombine(sc.getId());
            }
            //删除提单数据
            blnoMapper.deleteBatchIds(blIds);
        }
        //如果是自动批舱的记录需要把分配舱位数和冷插数还回去。
        List<SoVslvoyDO> vslvoyList = soVslvoyService.seleteBySoId(so.getId());
        List<Long> returnAllocationIdList = new ArrayList<>(8);
        List<Long> vslvoyConfirmUser = new ArrayList<>(8);
        for (SoVslvoyDO e : vslvoyList) {
            if (e.getAllocationId() != null) {
                returnAllocationIdList.add(e.getAllocationId());
            }
            if (ApproveStatusEnum.APPROVED.getValue().equals(e.getSpaceApproveStatus()) && TransmodeEnum.LINER.getValue().equals(e.getTransmode())) {
                try {
                    vslvoyConfirmUser.add(Long.parseLong(e.getUpdater()));
                } catch (NumberFormatException t) {
                    log.error("更新者转Long型失败， 更新者: {}", e.getUpdater());
                }
            }
        }
        if (CollectionUtil.isNotEmpty(returnAllocationIdList)) {
            List<RestoreSpace> restoreSpaceList = new ArrayList<>(8);
            restoreSpaceList.add(new RestoreSpace(so.getId(), returnAllocationIdList));
            autoSpaceService.restoreAllocatedSpace(restoreSpaceList);
        }
        Map<String, Object> sendEmailParameters = new HashMap<>(8);
        sendEmailParameters.put("bookingNo", so.getBookingNo());
        sendEmailParameters.put("bookingVessel", so.getBookingVessel());
        sendEmailParameters.put("bookingVoyage", so.getBookingVoyage());

        //给订舱确认人发送邮件通知。
        if (CollectionUtil.isNotEmpty(vslvoyConfirmUser)) {
            List<AdminUserDto> adminUsers = nucTransferService.getUserProfileByIdList(vslvoyConfirmUser);
            for (AdminUserDto adminUser : adminUsers) {
                if (Validator.isEmail(adminUser.getEmail())) {
                    taskExecutor.execute(() -> {
                        emailUtil.sendMail(adminUser.getEmail(), "customerCancelSoNotify", sendEmailParameters);
                    });
                }
            }
        }
        so.setSoStatus(SoStatusEnum.BookingCancelledByCustomer.getValue());
        soService.updateById(so);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @Lock4j(name = "soUnique", keys = {"'updateConfirmedSoAudit'", "#amendApply.id"})
    public void updateConfirmedSoAudit(SoAmendApplyUpdateReqVO amendApply, String userId) {
        SoAmendApplyDO amendApplyOld = soAmendApplyService.getSoAmendApply(amendApply.getId());
        if (amendApplyOld == null) {
            throw ServiceExceptionUtil.exception(SoAmendApplyErrorCodeConstants.SO_AMEND_APPLY_NOT_EXISTS);
        }
        if (!Objects.equals(amendApplyOld.getSoAmendStatus(), ContentChangeStatusEnum.BOOKING_CHANGE_PROCESSING.getValue())) {
            throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_BOOKING_DRAFT_CHANGE);
        }
        List<String> status = Arrays.asList(ContentChangeStatusEnum.BOOKING_CHANGE_CONFIRMED.getValue(), ContentChangeStatusEnum.BOOKING_CHANGE_REJECTED.getValue(), ContentChangeStatusEnum.BOOKING_CHANGE_CANCELLED_BY_CUSTOMER.getValue());
        if (!status.contains(amendApply.getSoAmendStatus())) {
            throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_UPDATE_CONFIRMED_APPLY_ERROR);
        }
        SoDO newSo = soService.getSo(amendApplyOld.getSoId());
        if (newSo == null) {
            throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_NOT_EXISTS);
        }
        // 新重新审核 ，旧的作废
        if (Objects.equals(ContentChangeStatusEnum.BOOKING_CHANGE_CONFIRMED.getValue(), amendApply.getSoAmendStatus())) {
            newSo.setSoContentStatus(SoContentStatusEnum.APPROVED.getValue());
            soService.updateById(newSo);
            soCommonService.syncSoStatus(newSo.getId(), SoContentStatusEnum.APPROVED.getValue());
        } else {
            newSo.setSoContentStatus(SoContentStatusEnum.REJECTED.getValue());
            soService.updateById(newSo);
            soCommonService.syncSoStatus(newSo.getId(), SoContentStatusEnum.REJECTED.getValue());
        }
        List<OperateLogReqVO> operateList = new ArrayList<>();
        operateList.add(new OperateLogReqVO(newSo.getId(), newSo.getBlNo()));
        List<OperateLogDO> operateLogList = operateLogService.batchInsert(operateList, SoSiEnum.SO.getValue(), OperateTypeEnum.SO_CONTENT_APPROVE.getValue(), amendApply.getSoAmendStatus(), amendApply.getReasonType(), amendApply.getDescCn());
        saveSoAndOperateLogList(Arrays.asList(soService.getSo(newSo.getId())), operateLogList, userId);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @Lock4j(name = "soUnique", keys = {"'amendCancel'", "#amendApplyId"})
    public void amendCancel(Long amendApplyId, String userId) {
        SoAmendApplyDO amendApplyOld = soAmendApplyService.getSoAmendApply(amendApplyId);
        if (amendApplyOld == null) {
            throw ServiceExceptionUtil.exception(SoAmendApplyErrorCodeConstants.SO_AMEND_APPLY_NOT_EXISTS);
        }
        checkUserLimit(amendApplyOld.getCreator(), userId);
        List<String> statusList = Arrays.asList(ContentChangeStatusEnum.BOOKING_CHANGE_PROCESSING.getValue(), ContentChangeStatusEnum.BOOKING_CHANGE_AWAITING_MATERIAL_UPLOADED.getValue(), ContentChangeStatusEnum.BOOKING_CANCEL_PROCESSING.getValue());
        if (statusList.contains(amendApplyOld.getSoAmendStatus()) == false) {
            throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_BOOKING_DRAFT_CHANGE);
        }
        SoDO newSo = soService.getSo(amendApplyOld.getSoId());
        if (newSo == null) {
            throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_NOT_EXISTS);
        }
        amendApplyOld.setSoAmendStatus(ContentChangeStatusEnum.BOOKING_CHANGE_CANCELLED_BY_CUSTOMER.getValue());
        soAmendApplyService.updateById(amendApplyOld);
        //作废
        if (Objects.equals(SoAmendOperateTypeEnum.AMEND_SO.getValue(), amendApplyOld.getOperatianType()) || Objects.equals(SoAmendOperateTypeEnum.CHANGE_VESSEL.getValue(), amendApplyOld.getOperatianType())) {
            newSo.setValidStatus(ValidStatusEnum.INVALID.getValue());
            soService.updateById(newSo);
        }
        if (Objects.equals(SoAmendOperateTypeEnum.CHANGE_VESSEL.getValue(), amendApplyOld.getOperatianType())) {
            specialModifyVelVoyService.updateSpecialApplyInfo(false, newSo.getOldId(), newSo.getId());
        }
        List<OperateLogReqVO> operateList = new ArrayList<>();
        operateList.add(new OperateLogReqVO(newSo.getId(), newSo.getBlNo()));
        List<OperateLogDO> operateLogList = operateLogService.batchInsert(operateList, SoSiEnum.SO.getValue(), OperateTypeEnum.SO_CONTENT_APPROVE.getValue(), ContentChangeStatusEnum.BOOKING_CHANGE_CANCELLED_BY_CUSTOMER.getValue(), "", "");
        saveSoAndOperateLogList(Arrays.asList(soService.getSo(newSo.getId())), operateLogList, userId);
    }

    public Map<String, String> getVesselOperatorByCodes(List<String> vesselCodeList) {
        //通过船名编码 获取船的详细信息
        Map<String, String> vesselOperatorMap = new HashMap<>(32);
        VesselNameListByCodeReqDTO codeReq = new VesselNameListByCodeReqDTO();
        codeReq.setVesselCodeList(vesselCodeList);
        CommonResult<List<VesselNameDataRespDTO>> result = vesselDataApi.listByCodeList(codeReq);
        if (result != null && CollectionUtil.isNotEmpty(result.getData())) {
            return result.getData().stream().filter(e -> StringUtils.isNotEmpty(e.getVesselOperator())).collect(Collectors.toMap(VesselNameDataRespDTO::getVesselCode, VesselNameDataRespDTO::getVesselOperator));
        }
        return vesselOperatorMap;
    }

    @Override
    public SoDetailCompareRespVo getAmendApplyDetail(Long amendApplyId) {
        SoAmendApplyDO amendApplyOld = soAmendApplyService.getSoAmendApply(amendApplyId);
        if (amendApplyOld == null) {
            throw ServiceExceptionUtil.exception(SoAmendApplyErrorCodeConstants.SO_AMEND_APPLY_NOT_EXISTS);
        }
        SoDetailRespVO newSo = getDetail(amendApplyOld.getSoId(), StringUtils.EMPTY, false);
        SoDetailRespVO oldSo = getDetail(newSo.getSo().getOldId(), StringUtils.EMPTY, false);
        return new SoDetailCompareRespVo(newSo, oldSo, SoAmendApplyConvert.INSTANCE.convert(amendApplyOld));
    }

    @Override
    public List<SoVslvoyScheduleVO> getLineScheduleByPolPod(PolPodReqDTO polPodReq) {
        return voyScheduleService.getLineScheduleByPolPod(polPodReq);
    }

    @Override
    public boolean checkCodTime(CodCheckReqVO reqVO) {
        boolean result = true;
        // 格式化当前时间
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
        String currentTime = LocalDateTime.now().format(formatter);

        // 调用接口查询是否存在截止时间记录
        VoyageBookingTimeDO voyageBookingTime = voyageBookingTimeService.getVoyageBookingTimeByVesselCodeAndVoyage(
                reqVO.getVesselCode(), reqVO.getVoyage(), reqVO.getPolCode(), reqVO.getImExType(), "CUSTOMS_CUT_OFF", currentTime);

        // 如果查询到记录，比较voyageBookingTime.getTimeValue()与当前时间
        if (voyageBookingTime != null && voyageBookingTime.getTimeValue() != null) {
            try {
                // 解析getTimeValue()为LocalDateTime
                LocalDateTime cutOffTime = LocalDateTime.parse(voyageBookingTime.getTimeValue(), formatter);
                LocalDateTime nowTime = LocalDateTime.parse(currentTime, formatter);
                // 如果截止时间小于当前时间，返回false；否则返回true
                result = cutOffTime.isAfter(nowTime) || cutOffTime.isEqual(nowTime);
            } catch (Exception e) {
                // 如果解析时间失败，默认返回true
                throw new RuntimeException("解析截止时间失败");
            }
        }

        // 检查SO的SI是否有签单和D/O
        try {
            // 创建查询条件
            SoPageReqVO soPageReqVO = new SoPageReqVO();
            soPageReqVO.setBookingVessel(reqVO.getVesselCode());
            soPageReqVO.setBookingVoyage(reqVO.getVoyage());
            soPageReqVO.setPolCode(reqVO.getPolCode());

            // 查询符合条件的SO列表
            PageResult<SoDO> soPageResult = soService.getSoPage(soPageReqVO);
            List<SoDO> soList = soPageResult.getList();

            // 检查每个SO的SI是否有签单和D/O
            if (CollectionUtil.isNotEmpty(soList)) {
                for (SoDO so : soList) {
                    // 根据订舱号查询对应的提单信息
                    BlnoPageReqVO blnoPageReqVO = new BlnoPageReqVO();
                    blnoPageReqVO.setBookingNo(so.getBookingNo());
                    PageResult<BlnoPageRespVO> blnoPageResult = blnoService.getBlnoPage(blnoPageReqVO);

                    // 检查每个提单的签单类型和D/O状态
                    if (CollectionUtil.isNotEmpty(blnoPageResult.getList())) {
                        for (BlnoPageRespVO blno : blnoPageResult.getList()) {
                            // 检查SI是否有签单：SIGN TYPE 不为空表示签单
                            if (blno.getSignType() != null && !blno.getSignType().isEmpty()) {
                                result = false;
                                break;
                            }

                            // 检查SI是否有D/O：DO status 不为空表示 D/O
                            if (blno.getDoStatus() != null && !blno.getDoStatus().isEmpty()) {
                                result = false;
                                break;
                            }
                        }

                        // 如果已经发现不满足条件的情况，跳出循环
                        if (!result) {
                            break;
                        }
                    }

                    // 检查SO本身的客户要求签单类型
                    if (so.getSignOrder() != null && !so.getSignOrder().isEmpty()) {
                        result = false;
                        break;
                    }
                }
            }
        } catch (Exception e) {
            // 如果查询或检查过程中出错，默认返回true
            throw new RuntimeException("检查SO的SI签单和D/O状态失败", e);
        }

        return result;
    }

    @Override
    public PageResult<SoRespVO> getSoManifest(ManifestSoPageReqVO pageVO) {
        Boolean flag = (StringUtils.isNotEmpty(pageVO.getBookingVessel()) && StringUtils.isNotEmpty(pageVO.getBookingVoyage())) || StringUtils.isNotEmpty(pageVO.getBookingNoEqual());
        if (flag) {
            pageVO.setSoStatusList(new String[]{SoStatusEnum.BookingConfirmed.getValue()});
            //查询主要信息
            PageResult<SoRespVO> pageResult = soService.getSoManifest(pageVO);
            // 拼装其他汇总信息
            addOtherSum(pageResult.getList());
            return new PageResult<>(pageResult.getList(), pageResult.getTotal());
        } else {
            throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_PARAM_ERROR);
        }
    }

    public void checkVslvoyAmend(List<SoVslvoyDO> soVslvoyList) {
        if (CollectionUtil.isNotEmpty(soVslvoyList)) {
            soVslvoyList.forEach(e -> {
                if (TransmodeEnum.LINER.getValue().equals(e.getTransmode())) {
                    //航次锁船记录
                    VoyageLockDO voyageLock = voyageLockService.findVoyageLock(e.getVesselCode(), e.getVoyageEx(), e.getPolCode(), ExImTypeEnum.EX.getValue());
                    if (voyageLock != null && voyageLock.getVoyageLockFlag()) {
                        throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_VOYAGE_LOCK_ERROR);
                    }
                }
            });
        }
    }

    public void checkVslvoy(List<SoVslvoyUpdateReqVO> soVslvoyList) {
        if (CollectionUtil.isNotEmpty(soVslvoyList)) {
            soVslvoyList.forEach(e -> {
                if (TransmodeEnum.LINER.getValue().equals(e.getTransmode())) {
                    //航次锁船记录
                    VoyageLockDO voyageLock = voyageLockService.findVoyageLock(e.getVesselCode(), e.getVoyageEx(), e.getPolCode(), ExImTypeEnum.EX.getValue());
                    if (voyageLock != null && voyageLock.getVoyageLockFlag()) {
                        throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_VOYAGE_LOCK_ERROR);
                    }
                    //全船接载
                    VoyageBsaDO voyageBsa = voyageBsaService.findVoyageBsa(e.getVesselCode(), e.getVoyageEx(), e.getPolCode(), ExImTypeEnum.EX.getValue());
                    if (voyageBsa != null && Arrays.asList(BookingOpenStatusEnum.WAITING.getValue(), BookingOpenStatusEnum.CLOSE.getValue()).contains(voyageBsa.getBookingOpenStatus())) {
                        throw ServiceExceptionUtil.exception(SoErrorCodeConstants.SO_VOYAGE_BSA_ERROR);
                    }
                    //在原航次截单之前
                    checkVoyageBookingTime(e.getVesselCode(), e.getVoyageEx(), e.getPolCode(), SI_CUT_OFF);
                }
            });
        }
    }


    public void checkMaintenanceDateBySoIds(List<Long> soIds) {
        Map<Long, List<SoVslvoyDO>> soVslvoyMap = soVslvoyService.seleteBySoIdsMap(new ArrayList<Long>(soIds));
        Map<Long, SoCargoDO> soCargoMap = soCargoService.selectBySoIds(new ArrayList<Long>(soIds));
        soIds.forEach(e -> {
            List<SoVslvoyDO> soVslvoyList = soVslvoyMap.get(e);
            SoCargoDO soCargo = soCargoMap.get(e);
            checkVesselOffHireMaintenanceDate(BeanUtil.copyProperties(soCargo, SoCargoBaseVO.class), soVslvoyList.stream().map(t -> t.getVesselCode()).collect(Collectors.toList()));
        });
    }

    public void checkVesselOffHireMaintenanceDate(SoCargoBaseVO cargo, List<String> vesselCodeList) {
        if (cargo != null && CollectionUtil.isNotEmpty(vesselCodeList)) {
            if (SoCargoBaseVO.checkSpecialCargo(cargo.getCargoType(), cargo.getSubCargoType())) {
                //订特种货时，不允许提交禁接的船舶
                vesselOffHireMaintenanceService.checkVesselOffHireMaintenanceDate(vesselCodeList, LocalDate.now());
            }
        }
    }
}
